diff --git a/src/vs/sessions/browser/parts/sessionCompositeBar.ts b/src/vs/sessions/browser/parts/sessionCompositeBar.ts index 5238fef1b8d..d9e72bd9a9e 100644 --- a/src/vs/sessions/browser/parts/sessionCompositeBar.ts +++ b/src/vs/sessions/browser/parts/sessionCompositeBar.ts @@ -19,10 +19,10 @@ import { IQuickInputService } from '../../../platform/quickinput/common/quickInp // eslint-disable-next-line local/code-import-patterns import { ISessionsManagementService } from '../../contrib/sessions/browser/sessionsManagementService.js'; // eslint-disable-next-line local/code-import-patterns -import { IChatData } from '../../contrib/sessions/common/sessionData.js'; +import { IChat } from '../../contrib/sessions/common/sessionData.js'; interface ISessionTab { - readonly chat: IChatData; + readonly chat: IChat; readonly element: HTMLElement; } @@ -83,7 +83,7 @@ export class SessionCompositeBar extends Disposable { this._register(this._themeService.onDidColorThemeChange(() => this._updateStyles())); } - private _rebuildTabs(chats: readonly IChatData[], activeChatId: string, mainChatId?: string): void { + private _rebuildTabs(chats: readonly IChat[], activeChatId: string, mainChatId?: string): void { this._tabDisposables.clear(); this._tabs.length = 0; reset(this._tabsContainer); @@ -96,7 +96,7 @@ export class SessionCompositeBar extends Disposable { this._updateVisibility(); } - private _createTab(chat: IChatData, isMainChat: boolean): void { + private _createTab(chat: IChat, isMainChat: boolean): void { const tab = $('.session-composite-bar-tab'); tab.tabIndex = 0; tab.setAttribute('role', 'tab'); @@ -152,7 +152,7 @@ export class SessionCompositeBar extends Disposable { this._tabs.push({ chat: chat, element: tab }); } - private _onTabClicked(chat: IChatData): void { + private _onTabClicked(chat: IChat): void { this._sessionsManagementService.openChat(chat.resource); } diff --git a/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts b/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts index 34dab27ff2f..6ed3fe11155 100644 --- a/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts +++ b/src/vs/sessions/contrib/chat/browser/extensionToolbarPickers.ts @@ -16,6 +16,7 @@ import { IChatSessionProviderOptionItem } from '../../../../workbench/contrib/ch import { ISessionOptionGroup } from './newSession.js'; import { RemoteNewSession } from '../../copilotChatSessions/browser/copilotChatSessionsProvider.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvidersService.js'; /** * Self-contained widget that renders extension-driven toolbar pickers @@ -35,6 +36,7 @@ export class ExtensionToolbarPickers extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, + @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, ) { super(); @@ -60,15 +62,16 @@ export class ExtensionToolbarPickers extends Disposable { } private _bindToSession(): void { - const chat = this.sessionsManagementService.activeSession.get()?.activeChat.get(); - if (!chat) { + const session = this.sessionsManagementService.activeSession.get(); + if (!session) { return; } - if (chat instanceof RemoteNewSession) { - this._renderToolbarPickers(chat, true); - this._sessionDisposables.add(chat.onDidChangeOptionGroups(() => { - this._renderToolbarPickers(chat); + const providerSession = this.sessionsProvidersService.getUntitledSession(session.providerId); + if (providerSession instanceof RemoteNewSession) { + this._renderToolbarPickers(providerSession, true); + this._sessionDisposables.add(providerSession.onDidChangeOptionGroups(() => { + this._renderToolbarPickers(providerSession); })); } } diff --git a/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts b/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts index 365f4747b3a..fcabc2bfefc 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts @@ -13,6 +13,7 @@ import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListOp import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvidersService.js'; import { CopilotCLISession } from '../../copilotChatSessions/browser/copilotChatSessionsProvider.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -60,16 +61,18 @@ export class NewChatPermissionPicker extends Disposable { @IDialogService private readonly dialogService: IDialogService, @IOpenerService private readonly openerService: IOpenerService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, + @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, ) { super(); // Write permission level to the active session data when it changes this._register(this.onDidChangeLevel(level => { - const chat = this.sessionsManagementService.activeSession.get()?.activeChat.get(); - if (!(chat instanceof CopilotCLISession)) { + const session = this.sessionsManagementService.activeSession.get(); + const providerSession = session ? this.sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + if (!(providerSession instanceof CopilotCLISession)) { throw new Error('NewChatPermissionPicker requires a CopilotCLISession'); } - chat.setPermissionLevel(level); + providerSession.setPermissionLevel(level); })); } diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index 9e15ce6b4a6..032729f1671 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -26,17 +26,14 @@ import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keyb import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; -import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; -import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { SessionsCategories } from '../../../common/categories.js'; import { IsActiveSessionBackgroundProviderContext, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { ISessionData, SessionStatus } from '../../sessions/common/sessionData.js'; +import { ISession } from '../../sessions/common/sessionData.js'; import { Menus } from '../../../browser/menus.js'; import { INonSessionTaskEntry, ISessionsConfigurationService, ISessionTaskWithTarget, ITaskEntry, TaskStorageTarget } from './sessionsConfigurationService.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; import { IRunScriptCustomTaskWidgetResult, RunScriptCustomTaskWidget } from './runScriptCustomTaskWidget.js'; -import { NewChatViewPane, SessionsViewId } from './newChatViewPane.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; @@ -93,7 +90,7 @@ function getPrimaryTask(tasks: readonly ISessionTaskWithTarget[], pinnedTaskLabe } interface IRunScriptActionContext { - readonly session: ISessionData; + readonly session: ISession; readonly tasks: readonly ISessionTaskWithTarget[]; readonly pinnedTaskLabel: string | undefined; } @@ -111,12 +108,10 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr private readonly _activeRunState: IObservable; constructor( - @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, + @ISessionsManagementService private readonly _sessionManagementService: ISessionsManagementService, @IKeybindingService _keybindingService: IKeybindingService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - @IViewsService private readonly _viewsService: IViewsService, @IActionViewItemService private readonly _actionViewItemService: IActionViewItemService, ) { super(); @@ -135,7 +130,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr && t1.task.runOptions?.runOn === t2.task.runOptions?.runOn); } }, reader => { - const activeSession = this._activeSessionService.activeSession.read(reader); + const activeSession = this._sessionManagementService.activeSession.read(reader); if (!activeSession) { return undefined; } @@ -164,8 +159,8 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr action, options, that._activeRunState, - (session: ISessionData) => that._showConfigureQuickPick(session), - (session: ISessionData, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => that._showCustomCommandInput(session, existingTask, mode), + (session: ISession) => that._showConfigureQuickPick(session), + (session: ISession, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => that._showCustomCommandInput(session, existingTask, mode), ); }, )); @@ -258,20 +253,13 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } async run(): Promise { - const status = session.status.read(undefined); - if (status === SessionStatus.Untitled) { - const viewPane = that._viewsService.getViewWithId(SessionsViewId); - viewPane?.sendQuery('/generate-run-commands'); - } else { - const widget = that._chatWidgetService.getWidgetBySessionResource(session.resource); - await widget?.acceptInput('/generate-run-commands'); - } + await that._sessionManagementService.sendAndCreateChat({ query: '/generate-run-commands' }, session); } })); })); } - private async _showConfigureQuickPick(session: ISessionData): Promise { + private async _showConfigureQuickPick(session: ISession): Promise { const nonSessionTasks = await this._sessionsConfigService.getNonSessionTasks(session); if (nonSessionTasks.length === 0) { // No existing tasks, go straight to custom command input @@ -320,7 +308,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } } - private async _showCustomCommandInput(session: ISessionData, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add'): Promise { + private async _showCustomCommandInput(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add'): Promise { const taskConfiguration = await this._showCustomCommandWidget(session, existingTask, mode); if (!taskConfiguration) { return undefined; @@ -374,7 +362,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr ); } - private _showCustomCommandWidget(session: ISessionData, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add'): Promise { + private _showCustomCommandWidget(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add'): Promise { const repo = session.workspace.get()?.repositories[0]; const workspaceTargetDisabledReason = !(repo?.workingDirectory ?? repo?.uri) ? localize('workspaceStorageUnavailableTooltip', "Workspace storage is unavailable for this session") @@ -450,16 +438,15 @@ class RunScriptActionViewItem extends BaseActionViewItem { action: IAction, _options: IActionViewItemOptions, private readonly _activeRunState: IObservable, - private readonly _showConfigureQuickPick: (session: ISessionData) => Promise, - private readonly _showCustomCommandInput: (session: ISessionData, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => Promise, + private readonly _showConfigureQuickPick: (session: ISession) => Promise, + private readonly _showCustomCommandInput: (session: ISession, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => Promise, @ICommandService private readonly _commandService: ICommandService, @ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService, @IContextKeyService contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - @IViewsService private readonly _viewsService: IViewsService, + @ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService, ) { super(undefined, action); @@ -677,13 +664,7 @@ class RunScriptActionViewItem extends BaseActionViewItem { class: undefined, category: addCategory, run: async () => { - if (session.status.get() === SessionStatus.Untitled) { - const viewPane = this._viewsService.getViewWithId(SessionsViewId); - viewPane?.sendQuery('/generate-run-commands'); - } else { - const widget = this._chatWidgetService.getWidgetBySessionResource(session.resource); - await widget?.acceptInput('/generate-run-commands'); - } + await this._sessionsManagementService.sendAndCreateChat({ query: '/generate-run-commands' }, session); }, }); diff --git a/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts index c412615f036..1535d45146d 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts @@ -34,8 +34,7 @@ export class SessionTypePicker extends Disposable { this._register(autorun(reader => { const session = this.sessionsManagementService.activeSession.read(reader); if (session) { - const chat = session.activeChat.read(reader); - this._sessionTypes = this.sessionsProvidersService.getSessionTypes(chat); + this._sessionTypes = this.sessionsProvidersService.getSessionTypesForProvider(session.providerId); this._sessionType = session.sessionType; } else { this._sessionTypes = []; diff --git a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts index 39d52cfe857..599e524c9fb 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts @@ -11,8 +11,7 @@ import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { ISessionData } from '../../sessions/common/sessionData.js'; +import { ISession } from '../../sessions/common/sessionData.js'; import { IJSONEditingService } from '../../../../workbench/services/configuration/common/jsonEditing.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IPreferencesService } from '../../../../workbench/services/preferences/common/preferences.js'; @@ -71,42 +70,42 @@ export interface ISessionsConfigurationService { * updated when the tasks.json file changes. Each entry includes the * storage target the task was loaded from. */ - getSessionTasks(session: ISessionData): IObservable; + getSessionTasks(session: ISession): IObservable; /** * Returns tasks that do NOT have `inSessions: true` — used as * suggestions in the "Add Run Action" picker. */ - getNonSessionTasks(session: ISessionData): Promise; + getNonSessionTasks(session: ISession): Promise; /** * Sets `inSessions: true` on an existing task (identified by label), * updating it in place in its tasks.json. */ - addTaskToSessions(task: ITaskEntry, session: ISessionData, target: TaskStorageTarget, options?: ITaskRunOptions): Promise; + addTaskToSessions(task: ITaskEntry, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise; /** * Creates a new shell task with `inSessions: true` and writes it to * the appropriate tasks.json (user or workspace). */ - createAndAddTask(label: string | undefined, command: string, session: ISessionData, target: TaskStorageTarget, options?: ITaskRunOptions): Promise; + createAndAddTask(label: string | undefined, command: string, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise; /** * Updates an existing task entry, optionally moving it between user and * workspace storage. */ - updateTask(originalTaskLabel: string, updatedTask: ITaskEntry, session: ISessionData, currentTarget: TaskStorageTarget, newTarget: TaskStorageTarget): Promise; + updateTask(originalTaskLabel: string, updatedTask: ITaskEntry, session: ISession, currentTarget: TaskStorageTarget, newTarget: TaskStorageTarget): Promise; /** * Removes an existing task entry from its tasks.json. */ - removeTask(taskLabel: string, session: ISessionData, target: TaskStorageTarget): Promise; + removeTask(taskLabel: string, session: ISession, target: TaskStorageTarget): Promise; /** * Runs a task via the task service, looking it up by label in the * workspace folder corresponding to the session worktree. */ - runTask(task: ITaskEntry, session: ISessionData): Promise; + runTask(task: ITaskEntry, session: ISession): Promise; /** * Observable label of the pinned task for the given repository. @@ -140,14 +139,13 @@ export class SessionsConfigurationService extends Disposable implements ISession @IPreferencesService private readonly _preferencesService: IPreferencesService, @ITaskService private readonly _taskService: ITaskService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - @ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService, @IStorageService private readonly _storageService: IStorageService, ) { super(); this._pinnedTaskLabels = this._loadPinnedTaskLabels(); } - getSessionTasks(session: ISessionData): IObservable { + getSessionTasks(session: ISession): IObservable { const repo = this._getSessionRepo(session); const folder = repo?.workingDirectory ?? repo?.uri; if (folder) { @@ -161,7 +159,7 @@ export class SessionsConfigurationService extends Disposable implements ISession return this._sessionTasks; } - async getNonSessionTasks(session: ISessionData): Promise { + async getNonSessionTasks(session: ISession): Promise { const result: INonSessionTaskEntry[] = []; const workspaceUri = this._getTasksJsonUri(session, 'workspace'); @@ -187,7 +185,7 @@ export class SessionsConfigurationService extends Disposable implements ISession return result; } - async addTaskToSessions(task: ITaskEntry, session: ISessionData, target: TaskStorageTarget, options?: ITaskRunOptions): Promise { + async addTaskToSessions(task: ITaskEntry, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise { const tasksJsonUri = this._getTasksJsonUri(session, target); if (!tasksJsonUri) { return; @@ -212,13 +210,9 @@ export class SessionsConfigurationService extends Disposable implements ISession } await this._jsonEditingService.write(tasksJsonUri, edits, true); - - if (target === 'workspace') { - await this._commitTasksFile(session); - } } - async createAndAddTask(label: string | undefined, command: string, session: ISessionData, target: TaskStorageTarget, options?: ITaskRunOptions): Promise { + async createAndAddTask(label: string | undefined, command: string, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise { const tasksJsonUri = this._getTasksJsonUri(session, target); if (!tasksJsonUri) { return undefined; @@ -240,14 +234,10 @@ export class SessionsConfigurationService extends Disposable implements ISession { path: ['tasks'], value: [...tasks, newTask] } ], true); - if (target === 'workspace') { - await this._commitTasksFile(session); - } - return newTask; } - async updateTask(originalTaskLabel: string, updatedTask: ITaskEntry, session: ISessionData, currentTarget: TaskStorageTarget, newTarget: TaskStorageTarget): Promise { + async updateTask(originalTaskLabel: string, updatedTask: ITaskEntry, session: ISession, currentTarget: TaskStorageTarget, newTarget: TaskStorageTarget): Promise { const currentTasksJsonUri = this._getTasksJsonUri(session, currentTarget); const newTasksJsonUri = this._getTasksJsonUri(session, newTarget); if (!currentTasksJsonUri || !newTasksJsonUri) { @@ -279,10 +269,6 @@ export class SessionsConfigurationService extends Disposable implements ISession ], true); } - if (currentTarget === 'workspace' || newTarget === 'workspace') { - await this._commitTasksFile(session); - } - const repoUri = this._getSessionRepo(session)?.uri; if (repoUri) { const key = repoUri.toString(); @@ -292,7 +278,7 @@ export class SessionsConfigurationService extends Disposable implements ISession } } - async removeTask(taskLabel: string, session: ISessionData, target: TaskStorageTarget): Promise { + async removeTask(taskLabel: string, session: ISession, target: TaskStorageTarget): Promise { const tasksJsonUri = this._getTasksJsonUri(session, target); if (!tasksJsonUri) { return; @@ -309,10 +295,6 @@ export class SessionsConfigurationService extends Disposable implements ISession { path: ['tasks'], value: tasks.filter((_, taskIndex) => taskIndex !== index) }, ], true); - if (target === 'workspace') { - await this._commitTasksFile(session); - } - const repoUri = this._getSessionRepo(session)?.uri; if (repoUri) { const key = repoUri.toString(); @@ -322,7 +304,7 @@ export class SessionsConfigurationService extends Disposable implements ISession } } - async runTask(task: ITaskEntry, session: ISessionData): Promise { + async runTask(task: ITaskEntry, session: ISession): Promise { const repo = this._getSessionRepo(session); const cwd = repo?.workingDirectory ?? repo?.uri; if (!cwd) { @@ -366,11 +348,11 @@ export class SessionsConfigurationService extends Disposable implements ISession // --- private helpers --- - private _getSessionRepo(session: ISessionData) { + private _getSessionRepo(session: ISession) { return session.workspace.get()?.repositories[0]; } - private _getTasksJsonUri(session: ISessionData, target: TaskStorageTarget): URI | undefined { + private _getTasksJsonUri(session: ISession, target: TaskStorageTarget): URI | undefined { if (target === 'workspace') { const repo = this._getSessionRepo(session); const folder = repo?.workingDirectory ?? repo?.uri; @@ -439,15 +421,6 @@ export class SessionsConfigurationService extends Disposable implements ISession transaction(tx => this._sessionTasks.set([...sessionTasks, ...userSessionTasks], tx)); } - private async _commitTasksFile(session: ISessionData): Promise { - const worktree = this._getSessionRepo(session)?.workingDirectory; // Only commit if there's a worktree. The local scenario does not need it - if (!worktree) { - return; - } - const tasksUri = joinPath(worktree, '.vscode', 'tasks.json'); - await this._sessionsManagementService.commitWorktreeFiles(session, [tasksUri]); - } - private _loadPinnedTaskLabels(): Map { const raw = this._storageService.get(SessionsConfigurationService._PINNED_TASK_LABELS_KEY, StorageScope.APPLICATION); if (raw) { diff --git a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts index 27f73414a99..3710898b735 100644 --- a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts @@ -20,10 +20,10 @@ import { VSBuffer } from '../../../../../base/common/buffer.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { Task } from '../../../../../workbench/contrib/tasks/common/tasks.js'; import { ITaskService } from '../../../../../workbench/contrib/tasks/common/taskService.js'; -import { IChatData, ISessionData, SessionStatus } from '../../../sessions/common/sessionData.js'; +import { IChat, ISession, SessionStatus } from '../../../sessions/common/sessionData.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -function makeSession(opts: { repository?: URI; worktree?: URI } = {}): ISessionData { +function makeSession(opts: { repository?: URI; worktree?: URI } = {}): ISession { const workspace = opts.repository ? { label: 'test', icon: Codicon.folder, @@ -36,7 +36,7 @@ function makeSession(opts: { repository?: URI; worktree?: URI } = {}): ISessionD }], requiresWorkspaceTrust: false, } : undefined; - const chat: IChatData = { + const chat: IChat = { chatId: 'test:session', resource: URI.parse('file:///session'), providerId: 'test', @@ -57,7 +57,7 @@ function makeSession(opts: { repository?: URI; worktree?: URI } = {}): ISessionD description: observableValue('description', undefined), pullRequest: observableValue('pullRequest', undefined), }; - const session: ISessionData = { ...chat, sessionId: chat.chatId, chats: observableValue('chats', [chat]), activeChat: observableValue('activeChat', chat), mainChat: chat }; + const session: ISession = { ...chat, sessionId: chat.chatId, chats: observableValue('chats', [chat]), activeChat: observableValue('activeChat', chat), mainChat: chat }; return session; } @@ -84,10 +84,9 @@ suite('SessionsConfigurationService', () => { let fileContents: Map; let jsonEdits: { uri: URI; values: IJSONValue[] }[]; let ranTasks: { label: string }[]; - let committedFiles: { session: ISessionData; fileUris: URI[] }[]; let storageService: InMemoryStorageService; let readFileCalls: URI[]; - let activeSessionObs: ReturnType>; + let activeSessionObs: ReturnType>; let tasksByLabel: Map; let workspaceFoldersByUri: Map; @@ -99,7 +98,6 @@ suite('SessionsConfigurationService', () => { fileContents = new Map(); jsonEdits = []; ranTasks = []; - committedFiles = []; readFileCalls = []; tasksByLabel = new Map(); workspaceFoldersByUri = new Map(); @@ -151,7 +149,6 @@ suite('SessionsConfigurationService', () => { instantiationService.stub(ISessionsManagementService, new class extends mock() { override activeSession = activeSessionObs; - override async commitWorktreeFiles(session: ISessionData, fileUris: URI[]) { committedFiles.push({ session, fileUris }); } }); storageService = store.add(new InMemoryStorageService()); @@ -306,8 +303,6 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(jsonEdits.length, 1); assert.deepStrictEqual(jsonEdits[0].values, [{ path: ['tasks', 1, 'inSessions'], value: true }]); - assert.strictEqual(committedFiles.length, 1); - assert.strictEqual(committedFiles[0].fileUris[0].path, '/worktree/.vscode/tasks.json'); }); test('addTaskToSessions does nothing when task label not found', async () => { @@ -335,7 +330,6 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(jsonEdits.length, 1); assert.strictEqual(jsonEdits[0].uri.toString(), repoTasksUri.toString()); assert.deepStrictEqual(jsonEdits[0].values, [{ path: ['tasks', 1, 'inSessions'], value: true }]); - assert.strictEqual(committedFiles.length, 0, 'should not commit when there is no worktree'); }); test('addTaskToSessions updates runOptions when provided', async () => { @@ -388,8 +382,6 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(tasks.length, 2); assert.strictEqual(tasks[1].label, 'npm run dev'); assert.strictEqual(tasks[1].inSessions, true); - assert.strictEqual(committedFiles.length, 1); - assert.strictEqual(committedFiles[0].fileUris[0].path, '/worktree/.vscode/tasks.json'); }); test('createAndAddTask writes to repository and does not commit when no worktree', async () => { @@ -409,7 +401,6 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(tasks.length, 2); assert.strictEqual(tasks[1].label, 'npm run dev'); assert.strictEqual(tasks[1].inSessions, true); - assert.strictEqual(committedFiles.length, 0, 'should not commit when there is no worktree'); }); test('createAndAddTask writes worktreeCreated run option when requested', async () => { @@ -462,8 +453,6 @@ suite('SessionsConfigurationService', () => { { label: 'lint', type: 'shell', command: 'npm run lint' }, ], }]); - assert.strictEqual(committedFiles.length, 1); - assert.strictEqual(committedFiles[0].fileUris[0].path, '/worktree/.vscode/tasks.json'); }); // --- updateTask --- @@ -495,7 +484,6 @@ suite('SessionsConfigurationService', () => { runOptions: { runOn: 'worktreeCreated' } } }]); - assert.strictEqual(committedFiles.length, 1); }); test('updateTask moves a task between workspace and user storage', async () => { @@ -542,7 +530,6 @@ suite('SessionsConfigurationService', () => { } ] }); - assert.strictEqual(committedFiles.length, 1); }); // --- pinned task --- diff --git a/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts index b1af990d5dd..509dc56c0e5 100644 --- a/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts +++ b/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts @@ -69,7 +69,7 @@ function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disp return; } - // Get changes from ISessionData + // Get changes from ISession const sessionData = sessionManagementService.getSession(resource); const changes = sessionData?.changes.get(); if (!changes || changes.length === 0) { diff --git a/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts index 2656f9e6164..88802105838 100644 --- a/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts +++ b/src/vs/sessions/contrib/codeReview/test/browser/codeReviewService.test.ts @@ -18,7 +18,7 @@ import { InMemoryStorageService, IStorageService, StorageScope, StorageTarget } import { IChatSessionFileChange, IChatSessionFileChange2 } from '../../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IGitHubService } from '../../../github/browser/githubService.js'; import { ISessionsChangeEvent, ISessionsManagementService } from '../../../sessions/browser/sessionsManagementService.js'; -import { ISessionData } from '../../../sessions/common/sessionData.js'; +import { ISession } from '../../../sessions/common/sessionData.js'; import { ICodeReviewService, CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion } from '../../browser/codeReviewService.js'; suite('CodeReviewService', () => { @@ -101,32 +101,32 @@ suite('CodeReviewService', () => { class MockSessionsManagementService extends mock() { private readonly _onDidChangeSessions: Emitter; override readonly onDidChangeSessions: Event; - override readonly activeSession: IObservable; + override readonly activeSession: IObservable; - private readonly _sessions = new Map(); + private readonly _sessions = new Map(); constructor(disposables: DisposableStore) { super(); this._onDidChangeSessions = disposables.add(new Emitter()); this.onDidChangeSessions = this._onDidChangeSessions.event; - this.activeSession = observableValue('test.activeSession', undefined); + this.activeSession = observableValue('test.activeSession', undefined); } - override getSession(resource: URI): ISessionData | undefined { + override getSession(resource: URI): ISession | undefined { return this._sessions.get(resource.toString()); } - addSession(resource: URI, changes?: readonly IChatSessionFileChange2[], archived = false): ISessionData { + addSession(resource: URI, changes?: readonly IChatSessionFileChange2[], archived = false): ISession { const changesObs = observableValue('test.changes', (changes ?? []).map(c => ({ modifiedUri: c.modifiedUri ?? c.uri, originalUri: c.originalUri, insertions: c.insertions, deletions: c.deletions })) ); const isArchivedObs = observableValue('test.isArchived', archived); - const sessionData: ISessionData = { + const sessionData: ISession = { sessionId: `test:${resource.toString()}`, resource, changes: changesObs, isArchived: isArchivedObs, - } as unknown as ISessionData; + } as unknown as ISession; this._sessions.set(resource.toString(), sessionData); return sessionData; } @@ -146,7 +146,7 @@ suite('CodeReviewService', () => { this._sessions.delete(resource.toString()); } - override getSessions(): ISessionData[] { + override getSessions(): ISession[] { return [...this._sessions.values()]; } diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/branchPicker.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/branchPicker.ts index f7300a7ab32..38dfa6b308c 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/branchPicker.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/branchPicker.ts @@ -12,6 +12,7 @@ import { IActionWidgetService } from '../../../../platform/actionWidget/browser/ import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvidersService.js'; import { CopilotCLISession } from './copilotChatSessionsProvider.js'; const FILTER_THRESHOLD = 10; @@ -34,26 +35,31 @@ export class BranchPicker extends Disposable { constructor( @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, + @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, ) { super(); this._register(autorun(reader => { const session = this.sessionsManagementService.activeSession.read(reader); - const chat = session?.activeChat.read(reader); - if (chat instanceof CopilotCLISession) { - chat.loading.read(reader); - chat.branches.read(reader); - chat.branchesLoading.read(reader); - chat.branchObservable.read(reader); - chat.isolationModeObservable.read(reader); + const providerSession = session ? this.sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + if (providerSession instanceof CopilotCLISession) { + providerSession.loading.read(reader); + providerSession.branches.read(reader); + providerSession.branchesLoading.read(reader); + providerSession.branchObservable.read(reader); + providerSession.isolationModeObservable.read(reader); } this._updateTriggerLabel(); })); } private _getSession(): CopilotCLISession | undefined { - const chat = this.sessionsManagementService.activeSession.get()?.activeChat.get(); - return chat instanceof CopilotCLISession ? chat : undefined; + const session = this.sessionsManagementService.activeSession.get(); + if (!session) { + return undefined; + } + const providerSession = this.sessionsProvidersService.getUntitledSession(session.providerId); + return providerSession instanceof CopilotCLISession ? providerSession : undefined; } render(container: HTMLElement): void { diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsActions.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsActions.ts index 4a5b185ff0d..5bbb4042257 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsActions.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsActions.ts @@ -25,7 +25,7 @@ import { Menus } from '../../../browser/menus.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvidersService.js'; import { SessionItemContextMenuId } from '../../sessions/browser/views/sessionsList.js'; -import { ISessionData } from '../../sessions/common/sessionData.js'; +import { ISession } from '../../sessions/common/sessionData.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { CopilotCLISession, COPILOT_PROVIDER_ID } from './copilotChatSessionsProvider.js'; import { COPILOT_CLI_SESSION_TYPE, COPILOT_CLOUD_SESSION_TYPE } from '../../sessions/browser/sessionTypes.js'; @@ -289,6 +289,7 @@ class CopilotActiveSessionContribution extends Disposable implements IWorkbenchC constructor( @ISessionsManagementService sessionsManagementService: ISessionsManagementService, + @ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService, @IContextKeyService contextKeyService: IContextKeyService, ) { super(); @@ -297,10 +298,10 @@ class CopilotActiveSessionContribution extends Disposable implements IWorkbenchC this._register(autorun((reader: IReader) => { const session = sessionsManagementService.activeSession.read(reader); - const chat = session?.activeChat.read(reader); - if (chat instanceof CopilotCLISession) { - const isLoading = chat.loading.read(reader); - hasRepositoryKey.set(!isLoading && !!chat.gitRepository); + const providerSession = session ? sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + if (providerSession instanceof CopilotCLISession) { + const isLoading = providerSession.loading.read(reader); + hasRepositoryKey.set(!isLoading && !!providerSession.gitRepository); } else { hasRepositoryKey.set(false); } @@ -314,7 +315,7 @@ registerWorkbenchContribution2(CopilotActiveSessionContribution.ID, CopilotActiv /** * Bridges extension-contributed context menu actions from {@link MenuId.AgentSessionsContext} * to {@link SessionItemContextMenuId} for the new sessions view. - * Registers wrapper commands that resolve {@link ISessionData} → {@link IAgentSession} + * Registers wrapper commands that resolve {@link ISession} → {@link IAgentSession} * and forward to the original command with marshalled context. */ class CopilotSessionContextMenuBridge extends Disposable implements IWorkbenchContribution { @@ -348,7 +349,7 @@ class CopilotSessionContextMenuBridge extends Disposable implements IWorkbenchCo this._bridgedIds.add(commandId); const wrapperId = `sessionsViewPane.bridge.${commandId}`; - this._register(CommandsRegistry.registerCommand(wrapperId, (accessor, sessionData?: ISessionData) => { + this._register(CommandsRegistry.registerCommand(wrapperId, (accessor, sessionData?: ISession) => { if (!sessionData) { return; } diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts index 34fa0538a0f..5e589000c54 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts @@ -18,10 +18,10 @@ import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browse import { AgentSessionProviders, AgentSessionTarget } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { IChatService, IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { ChatSessionStatus, IChatSessionFileChange, IChatSessionsService, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; -import { IChatData, ISessionRepository, ISessionWorkspace, SessionStatus, GITHUB_REMOTE_FILE_SCHEME, ISessionPullRequest } from '../../sessions/common/sessionData.js'; +import { ISessionData, ISessionRepository, ISessionWorkspace, SessionStatus, GITHUB_REMOTE_FILE_SCHEME, ISessionPullRequest } from '../../sessions/common/sessionData.js'; import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js'; import { basename } from '../../../../base/common/resources.js'; -import { ISendRequestOptions, ISessionsBrowseAction, IChatChangeEvent, ISessionsProvider, ISessionType } from '../../sessions/browser/sessionsProvider.js'; +import { ISendRequestOptions, ISessionsBrowseAction, ISessionChangeEvent, ISessionsProvider, ISessionType } from '../../sessions/browser/sessionsProvider.js'; import { ISessionOptionGroup } from '../../chat/browser/newSession.js'; import { IsolationMode } from './isolationPicker.js'; import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; @@ -52,7 +52,7 @@ const AGENT_OPTION_ID = 'agent'; * Provider-specific observable fields on new Copilot sessions. * Used by pickers and contributions that need to read/write provider-internal state. */ -export interface ICopilotNewSessionData extends IChatData { +export interface ICopilotNewSessionData extends ISessionData { readonly permissionLevel: IObservable; readonly branchObservable: IObservable; readonly isolationModeObservable: IObservable; @@ -60,16 +60,16 @@ export interface ICopilotNewSessionData extends IChatData { /** * Local new session for Background agent sessions. - * Implements {@link IChatData} (session facade) and provides + * Implements {@link ISessionData} (session facade) and provides * pre-send configuration methods for the new-session flow. */ -export class CopilotCLISession extends Disposable implements IChatData { +export class CopilotCLISession extends Disposable implements ISessionData { static readonly COPILOT_WORKTREE_PATTERN = 'copilot-worktree-'; - // -- IChatData fields -- + // -- ISessionData fields -- - readonly chatId: string; + readonly id: string; readonly providerId: string; readonly sessionType: string; readonly icon: ThemeIcon; @@ -164,7 +164,7 @@ export class CopilotCLISession extends Disposable implements IChatData { @IGitService private readonly gitService: IGitService, ) { super(); - this.chatId = `${providerId}:${resource.toString()}`; + this.id = `${providerId}:${resource.toString()}`; this.providerId = providerId; this.sessionType = AgentSessionProviders.Background; this.icon = CopilotCLISessionType.icon; @@ -176,7 +176,7 @@ export class CopilotCLISession extends Disposable implements IChatData { this.setOption(REPOSITORY_OPTION_ID, repoUri.fsPath); } - // Set IChatData workspace observable + // Set ISessionData workspace observable this._workspaceData.set(sessionWorkspace, undefined); this._isolationMode = 'worktree'; @@ -297,7 +297,7 @@ export class CopilotCLISession extends Disposable implements IChatData { this.chatSessionsService.setSessionOption(this.resource, optionId, value); } - update(session: IChatData): void { + update(session: ISessionData): void { this._workspaceData.set(session.workspace.get(), undefined); } } @@ -316,14 +316,14 @@ function isRepositoriesOptionGroup(group: IChatSessionProviderOptionGroup): bool /** * Remote new session for Cloud agent sessions. - * Implements {@link IChatData} (session facade) and provides + * Implements {@link ISessionData} (session facade) and provides * pre-send configuration methods for the new-session flow. */ -export class RemoteNewSession extends Disposable implements IChatData { +export class RemoteNewSession extends Disposable implements ISessionData { - // -- IChatData fields -- + // -- ISessionData fields -- - readonly chatId: string; + readonly id: string; readonly providerId: string; readonly sessionType: string; readonly icon: ThemeIcon; @@ -397,7 +397,7 @@ export class RemoteNewSession extends Disposable implements IChatData { @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); - this.chatId = `${providerId}:${resource.toString()}`; + this.id = `${providerId}:${resource.toString()}`; this.providerId = providerId; this.sessionType = target; this.icon = CopilotCloudSessionType.icon; @@ -533,7 +533,7 @@ export class RemoteNewSession extends Disposable implements IChatData { return group.items.find(i => i.default === true) ?? group.items[0]; } - update(session: IChatData): void { } + update(session: ISessionData): void { } } /** @@ -553,11 +553,11 @@ function toSessionStatus(status: ChatSessionStatus): SessionStatus { } /** - * Adapts an existing {@link IAgentSession} from the chat layer into the new {@link IChatData} facade. + * Adapts an existing {@link IAgentSession} from the chat layer into the new {@link ISessionData} facade. */ -class AgentSessionAdapter implements IChatData { +class AgentSessionAdapter implements ISessionData { - readonly chatId: string; + readonly id: string; readonly resource: URI; readonly providerId: string; readonly sessionType: string; @@ -602,7 +602,7 @@ class AgentSessionAdapter implements IChatData { session: IAgentSession, providerId: string, ) { - this.chatId = `${providerId}:${session.resource.toString()}`; + this.id = `${providerId}:${session.resource.toString()}`; this.resource = session.resource; this.providerId = providerId; this.sessionType = session.providerType; @@ -816,8 +816,8 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions readonly icon = Codicon.copilot; readonly sessionTypes: readonly ISessionType[] = [CopilotCLISessionType, CopilotCloudSessionType]; - private readonly _onDidChangeSessions = this._register(new Emitter()); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + private readonly _onDidChangeSessions = this._register(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; /** Cache of adapted sessions, keyed by resource URI string. */ private readonly _sessionCache = new Map(); @@ -862,7 +862,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions // -- Sessions -- - getSessionTypes(session: IChatData): ISessionType[] { + getSessionTypes(session: ISessionData): ISessionType[] { if (session instanceof CopilotCLISession) { return [CopilotCLISessionType]; } @@ -872,7 +872,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions return []; } - getSessions(): IChatData[] { + getSessions(): ISessionData[] { this._ensureSessionCache(); return Array.from(this._sessionCache.values()); } @@ -881,7 +881,11 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions private _currentNewSession: (CopilotCLISession | RemoteNewSession) | undefined; - createNewSession(workspace: ISessionWorkspace): IChatData { + getUntitledSession(): ISessionData | undefined { + return this._currentNewSession; + } + + createNewSession(workspace: ISessionWorkspace): ISessionData { const workspaceUri = workspace.repositories[0]?.uri; if (!workspaceUri) { throw new Error('Workspace has no repository URI'); @@ -905,7 +909,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions return session; } - createNewSessionFrom(chatId: string): IChatData { + createNewSessionFrom(chatId: string): ISessionData { const chat = this._findChatSession(chatId); if (!chat) { throw new Error(`Session '${chatId}' not found`); @@ -938,12 +942,12 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions return session; } - setSessionType(chatId: string, type: ISessionType): IChatData { + setSessionType(chatId: string, type: ISessionType): ISessionData { throw new Error('Session type cannot be changed'); } setModel(chatId: string, modelId: string): void { - if (this._currentNewSession?.chatId === chatId) { + if (this._currentNewSession?.id === chatId) { this._currentNewSession.setModelId(modelId); } } @@ -996,9 +1000,9 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions // -- Send -- - async sendRequest(chatId: string, options: ISendRequestOptions): Promise { + async sendRequest(chatId: string, options: ISendRequestOptions): Promise { const session = this._currentNewSession; - if (!session || session.chatId !== chatId) { + if (!session || session.id !== chatId) { throw new Error(`Session '${chatId}' not found or not a new session`); } @@ -1084,12 +1088,12 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions return createdSession; } - private async _waitForNewAgentSession(target: AgentSessionTarget, existingSessions: ResourceSet): Promise { + private async _waitForNewAgentSession(target: AgentSessionTarget, existingSessions: ResourceSet): Promise { const found = [...this._sessionCache.values()].find(s => s.sessionType === target && !existingSessions.has(s.resource)); if (found) { return found; } - return new Promise(resolve => { + return new Promise(resolve => { const listener = this.onDidChangeSessions((e) => { const s = e.added.find(s => s.sessionType === target && !existingSessions.has(s.resource)); if (s) { @@ -1166,8 +1170,8 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions private _refreshSessionCache(): void { const currentKeys = new Set(); - const added: IChatData[] = []; - const changed: IChatData[] = []; + const added: ISessionData[] = []; + const changed: ISessionData[] = []; for (const session of this.agentSessionsService.model.sessions) { if (session.resource.toString() === this._currentNewSession?.resource.toString()) { @@ -1194,7 +1198,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions } } - const removed: IChatData[] = []; + const removed: ISessionData[] = []; for (const [key, adapter] of this._sessionCache) { if (!currentKeys.has(key)) { this._sessionCache.delete(key); @@ -1207,7 +1211,7 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions } } - private _findChatSession(chatId: string): IChatData | undefined { + private _findChatSession(chatId: string): ISessionData | undefined { return this._sessionCache.get(this._localIdFromchatId(chatId)); } diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/isolationPicker.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/isolationPicker.ts index cbf923c7cda..aa0f455c61d 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/isolationPicker.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/isolationPicker.ts @@ -13,6 +13,7 @@ import { IActionWidgetService } from '../../../../platform/actionWidget/browser/ import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvidersService.js'; import { CopilotCLISession } from './copilotChatSessionsProvider.js'; export type IsolationMode = 'worktree' | 'workspace'; @@ -42,6 +43,7 @@ export class IsolationPicker extends Disposable { @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, @IConfigurationService private readonly configurationService: IConfigurationService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, + @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, ) { super(); this._isolationOptionEnabled = this.configurationService.getValue('github.copilot.chat.cli.isolationOption.enabled') !== false; @@ -58,12 +60,12 @@ export class IsolationPicker extends Disposable { this._register(autorun(reader => { const session = this.sessionsManagementService.activeSession.read(reader); - const chat = session?.activeChat.read(reader); - if (chat instanceof CopilotCLISession) { - const isLoading = chat.loading.read(reader); - this._hasGitRepo = !isLoading && !!chat.gitRepository; + const providerSession = session ? this.sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + if (providerSession instanceof CopilotCLISession) { + const isLoading = providerSession.loading.read(reader); + this._hasGitRepo = !isLoading && !!providerSession.gitRepository; // Read isolation mode from session — session is the source of truth - chat.isolationModeObservable.read(reader); + providerSession.isolationModeObservable.read(reader); } else { this._hasGitRepo = false; } @@ -72,8 +74,9 @@ export class IsolationPicker extends Disposable { } private _getSessionIsolationMode(): IsolationMode { - const session = this.sessionsManagementService.activeSession.get()?.activeChat.get(); - return session instanceof CopilotCLISession ? session.isolationMode : 'worktree'; + const session = this.sessionsManagementService.activeSession.get(); + const providerSession = session ? this.sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + return providerSession instanceof CopilotCLISession ? providerSession.isolationMode : 'worktree'; } render(container: HTMLElement): void { @@ -151,11 +154,12 @@ export class IsolationPicker extends Disposable { } private _setModeOnSession(mode: IsolationMode): void { - const session = this.sessionsManagementService.activeSession.get()?.activeChat.get(); - if (!(session instanceof CopilotCLISession)) { + const session = this.sessionsManagementService.activeSession.get(); + const providerSession = session ? this.sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + if (!(providerSession instanceof CopilotCLISession)) { throw new Error('IsolationPicker requires a CopilotCLISession'); } - session.setIsolationMode(mode); + providerSession.setIsolationMode(mode); } private _updateTriggerLabel(): void { diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/modePicker.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/modePicker.ts index 597888f97d9..b1292ffc9b1 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/modePicker.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/modePicker.ts @@ -18,6 +18,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { Target } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { AICustomizationManagementCommands } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvidersService.js'; import { CopilotCLISession } from './copilotChatSessionsProvider.js'; interface IModePickerItem { @@ -57,6 +58,7 @@ export class ModePicker extends Disposable { @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @ICommandService private readonly commandService: ICommandService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, + @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, ) { super(); @@ -212,9 +214,10 @@ export class ModePicker extends Disposable { this._updateTriggerLabel(); this._onDidChange.fire(mode); - const chat = this.sessionsManagementService.activeSession.get()?.activeChat.get(); - if (chat instanceof CopilotCLISession) { - chat.setMode(mode); + const session = this.sessionsManagementService.activeSession.get(); + const providerSession = session ? this.sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + if (providerSession instanceof CopilotCLISession) { + providerSession.setMode(mode); } } diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/modelPicker.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/modelPicker.ts index 978a2dae27d..c8dbf57dfc6 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/modelPicker.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/modelPicker.ts @@ -14,6 +14,7 @@ import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../ import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { ISessionsProvidersService } from '../../sessions/browser/sessionsProvidersService.js'; import { RemoteNewSession } from './copilotChatSessionsProvider.js'; const FILTER_THRESHOLD = 10; @@ -50,15 +51,16 @@ export class CloudModelPicker extends Disposable { constructor( @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, @ISessionsManagementService sessionsManagementService: ISessionsManagementService, + @ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService, @IChatSessionsService chatSessionsService: IChatSessionsService, ) { super(); this._register(autorun(reader => { const session = sessionsManagementService.activeSession.read(reader); - const chat = session?.activeChat.read(reader); - if (chat instanceof RemoteNewSession) { - this._setSession(chat); + const providerSession = session ? sessionsProvidersService.getUntitledSession(session.providerId) : undefined; + if (providerSession instanceof RemoteNewSession) { + this._setSession(providerSession); } })); diff --git a/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts b/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts index 07c84810bd4..13663448e19 100644 --- a/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts +++ b/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts @@ -43,7 +43,7 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { ISessionData, GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionData.js'; +import { ISession, GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionData.js'; import { getGitHubRemoteFileDisplayName } from './githubFileSystemProvider.js'; import { basename } from '../../../../base/common/path.js'; import { isEqual } from '../../../../base/common/resources.js'; @@ -236,10 +236,10 @@ export class FileTreeViewPane extends ViewPane { /** * Determines the root URI for the file tree based on the active session type. - * Tries multiple data sources: ISessionData workspace, agent session model metadata, + * Tries multiple data sources: ISession workspace, agent session model metadata, * and file change URIs as a last resort. */ - private resolveTreeRoot(activeSession: ISessionData | undefined): URI | undefined { + private resolveTreeRoot(activeSession: ISession | undefined): URI | undefined { if (!activeSession) { return undefined; } diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts index 0525bb3ae34..4f7411d06fd 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts @@ -23,9 +23,9 @@ import { IChatSendRequestOptions, IChatService } from '../../../../workbench/con import { IChatSessionFileChange, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; -import { IChatChangeEvent, ISendRequestOptions, ISessionsBrowseAction, ISessionsProvider, ISessionType } from '../../sessions/browser/sessionsProvider.js'; +import { ISessionChangeEvent, ISendRequestOptions, ISessionsBrowseAction, ISessionsProvider, ISessionType } from '../../sessions/browser/sessionsProvider.js'; import { CopilotCLISessionType } from '../../sessions/browser/sessionTypes.js'; -import { IChatData, ISessionPullRequest, ISessionWorkspace, SessionStatus } from '../../sessions/common/sessionData.js'; +import { ISessionData, ISessionPullRequest, ISessionWorkspace, SessionStatus } from '../../sessions/common/sessionData.js'; import { IRemoteAgentHostConnectionInfo } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; export interface IRemoteAgentHostSessionsProviderConfig { @@ -34,11 +34,11 @@ export interface IRemoteAgentHostSessionsProviderConfig { } /** - * Adapts agent host session metadata into the {@link IChatData} facade. + * Adapts agent host session metadata into the {@link ISessionData} facade. */ -class RemoteSessionAdapter implements IChatData { +class RemoteSessionAdapter implements ISessionData { - readonly chatId: string; + readonly id: string; readonly resource: URI; readonly providerId: string; readonly sessionType: string; @@ -72,7 +72,7 @@ class RemoteSessionAdapter implements IChatData { const rawId = AgentSession.id(metadata.session); this.agentProvider = AgentSession.provider(metadata.session) ?? 'copilot'; this.resource = URI.from({ scheme: resourceScheme, path: `/${rawId}` }); - this.chatId = `${providerId}:${this.resource.toString()}`; + this.id = `${providerId}:${this.resource.toString()}`; this.providerId = providerId; this.sessionType = logicalSessionType; this.createdAt = new Date(metadata.startTime); @@ -117,8 +117,8 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess readonly icon: ThemeIcon = Codicon.remote; readonly sessionTypes: readonly ISessionType[]; - private readonly _onDidChangeSessions = this._register(new Emitter()); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + private readonly _onDidChangeSessions = this._register(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; readonly browseActions: readonly ISessionsBrowseAction[]; @@ -211,20 +211,24 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess // -- Sessions -- - getSessionTypes(_chat: IChatData): ISessionType[] { + getSessionTypes(_chat: ISessionData): ISessionType[] { return [...this.sessionTypes]; } - getSessions(): IChatData[] { + getSessions(): ISessionData[] { this._ensureSessionCache(); return Array.from(this._sessionCache.values()); } // -- Session Lifecycle -- - private _currentNewSession: IChatData | undefined; + private _currentNewSession: ISessionData | undefined; - createNewSession(workspace: ISessionWorkspace): IChatData { + getUntitledSession(): ISessionData | undefined { + return this._currentNewSession; + } + + createNewSession(workspace: ISessionWorkspace): ISessionData { const workspaceUri = workspace.repositories[0]?.uri; if (!workspaceUri) { throw new Error('Workspace has no repository URI'); @@ -235,8 +239,8 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess this._selectedModelId = undefined; const resource = URI.from({ scheme: this._sessionTypeForProvider('copilot'), path: `/untitled-${generateUuid()}` }); - const session: IChatData = { - chatId: `${this.id}:${resource.toString()}`, + const session: ISessionData = { + id: `${this.id}:${resource.toString()}`, resource, providerId: this.id, sessionType: this.sessionTypes[0].id, @@ -260,16 +264,16 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess return session; } - createNewSessionFrom(_chatId: string): IChatData { + createNewSessionFrom(_chatId: string): ISessionData { throw new Error('Remote agent host sessions do not support forking'); } - setSessionType(_chatId: string, _type: ISessionType): IChatData { + setSessionType(_chatId: string, _type: ISessionType): ISessionData { throw new Error('Remote agent host sessions do not support changing session type'); } setModel(chatId: string, modelId: string): void { - if (this._currentNewSession?.chatId === chatId) { + if (this._currentNewSession?.id === chatId) { this._selectedModelId = modelId; } } @@ -306,9 +310,9 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess } } - async sendRequest(chatId: string, options: ISendRequestOptions): Promise { + async sendRequest(chatId: string, options: ISendRequestOptions): Promise { const session = this._currentNewSession; - if (!session || session.chatId !== chatId) { + if (!session || session.id !== chatId) { throw new Error(`Session '${chatId}' not found or not a new session`); } @@ -386,8 +390,8 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess try { const sessions = await this._connection.listSessions(); const currentKeys = new Set(); - const added: IChatData[] = []; - const changed: IChatData[] = []; + const added: ISessionData[] = []; + const changed: ISessionData[] = []; for (const meta of sessions) { const rawId = AgentSession.id(meta.session); @@ -405,7 +409,7 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess } } - const removed: IChatData[] = []; + const removed: ISessionData[] = []; for (const [key, cached] of this._sessionCache) { if (!currentKeys.has(key)) { this._sessionCache.delete(key); @@ -425,7 +429,7 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess * Wait for a new session to appear in the cache that wasn't present before. * Tries an immediate refresh, then listens for the session-added notification. */ - private async _waitForNewSession(existingKeys: Set): Promise { + private async _waitForNewSession(existingKeys: Set): Promise { // First, try an immediate refresh await this._refreshSessions(CancellationToken.None); for (const [key, cached] of this._sessionCache) { @@ -435,7 +439,7 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess } // If not found yet, wait for the next onDidChangeSessions event - return new Promise(resolve => { + return new Promise(resolve => { const listener = this._onDidChangeSessions.event(e => { const newSession = e.added.find(s => { const rawId = s.resource.path.substring(1); diff --git a/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts index 45da7d4b967..ed190e132b7 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts @@ -20,7 +20,7 @@ import { IChatService, type ChatSendResult } from '../../../../../workbench/cont import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js'; import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js'; import { SessionStatus } from '../../../sessions/common/sessionData.js'; -import { IChatChangeEvent } from '../../../sessions/browser/sessionsProvider.js'; +import { ISessionChangeEvent } from '../../../sessions/browser/sessionsProvider.js'; import { CopilotCLISessionType } from '../../../sessions/browser/sessionTypes.js'; import { RemoteAgentHostSessionsProvider, type IRemoteAgentHostSessionsProviderConfig } from '../../browser/remoteAgentHostSessionsProvider.js'; @@ -194,8 +194,8 @@ suite('RemoteAgentHostSessionsProvider', () => { test('onDidChangeSessions fires when session added notification arrives', () => { const provider = createProvider(disposables, connection); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); fireSessionAdded(connection, 'notif-1', { title: 'Notif Session' }); @@ -206,8 +206,8 @@ suite('RemoteAgentHostSessionsProvider', () => { test('accepts session notifications from any agent provider', () => { const provider = createProvider(disposables, connection); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); fireSessionAdded(connection, 'other-sess', { provider: 'other-agent', title: 'Other Session' }); @@ -219,8 +219,8 @@ suite('RemoteAgentHostSessionsProvider', () => { const provider = createProvider(disposables, connection); fireSessionAdded(connection, 'to-remove', { title: 'Removed' }); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); fireSessionRemoved(connection, 'to-remove'); @@ -230,8 +230,8 @@ suite('RemoteAgentHostSessionsProvider', () => { test('duplicate session added notification is ignored', () => { const provider = createProvider(disposables, connection); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); fireSessionAdded(connection, 'dup-sess', { title: 'Dup' }); fireSessionAdded(connection, 'dup-sess', { title: 'Dup' }); @@ -241,8 +241,8 @@ suite('RemoteAgentHostSessionsProvider', () => { test('removing non-existent session is no-op', () => { const provider = createProvider(disposables, connection); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); fireSessionRemoved(connection, 'does-not-exist'); @@ -254,8 +254,8 @@ suite('RemoteAgentHostSessionsProvider', () => { fireSessionAdded(connection, 'cross-prov', { provider: 'other-agent', title: 'Cross Provider' }); assert.strictEqual(provider.getSessions().length, 1); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); fireSessionRemoved(connection, 'cross-prov', 'other-agent'); @@ -271,8 +271,8 @@ suite('RemoteAgentHostSessionsProvider', () => { connection.addSession(createSession('list-2', { summary: 'Second' })); const provider = createProvider(disposables, connection); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); provider.getSessions(); await new Promise(resolve => setTimeout(resolve, 50)); @@ -325,7 +325,7 @@ suite('RemoteAgentHostSessionsProvider', () => { const target = sessions.find((s) => s.title.get() === 'To Delete'); assert.ok(target, 'Session should exist'); - await provider.deleteSession(target!.chatId); + await provider.deleteSession(target!.id); assert.strictEqual(connection.disposedSessions.length, 1); // The disposed URI must be a backend agent session URI (copilot://del-sess), @@ -347,7 +347,7 @@ suite('RemoteAgentHostSessionsProvider', () => { assert.ok(target, 'Session should exist'); assert.strictEqual(target!.isRead.get(), true); - provider.setRead(target!.chatId, false); + provider.setRead(target!.id, false); assert.strictEqual(target!.isRead.get(), false); }); @@ -417,8 +417,8 @@ suite('RemoteAgentHostSessionsProvider', () => { // Update on connection side connection.addSession(createSession('turn-sess', { summary: 'After', modifiedTime: 5000 })); - const changes: IChatChangeEvent[] = []; - disposables.add(provider.onDidChangeSessions((e: IChatChangeEvent) => changes.push(e))); + const changes: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions((e: ISessionChangeEvent) => changes.push(e))); connection.fireAction({ action: { diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 525e51a3ca5..8af865126ea 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { IObservable, IReader, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -14,19 +14,18 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ISessionsProvidersService } from './sessionsProvidersService.js'; -import { ISessionType, ISendRequestOptions, IChatChangeEvent } from './sessionsProvider.js'; +import { ISessionType, ISendRequestOptions, ISessionChangeEvent } from './sessionsProvider.js'; import { SessionsGroupModel } from './sessionsGroupModel.js'; -import { ISessionData, ISessionWorkspace, GITHUB_REMOTE_FILE_SCHEME, IChatData } from '../common/sessionData.js'; +import { ISession, ISessionWorkspace, GITHUB_REMOTE_FILE_SCHEME, ISessionData, IChat, SessionStatus } from '../common/sessionData.js'; import { IGitHubSessionContext } from '../../github/common/types.js'; import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; /** * Configuration properties available on new/pending sessions. - * Not part of the public {@link ISessionData} contract but present on + * Not part of the public {@link ISession} contract but present on * concrete session implementations (CopilotCLISession, RemoteNewSession, AgentHostNewSession). */ @@ -53,9 +52,9 @@ const ACTIVE_PROVIDER_KEY = 'sessions.activeProviderId'; * Event fired when sessions change within a provider. */ export interface ISessionsChangeEvent { - readonly added: readonly ISessionData[]; - readonly removed: readonly ISessionData[]; - readonly changed: readonly ISessionData[]; + readonly added: readonly ISession[]; + readonly removed: readonly ISession[]; + readonly changed: readonly ISession[]; } /** @@ -71,12 +70,12 @@ export interface ISessionsManagementService { /** * Get all sessions from all registered providers. */ - getSessions(): ISessionData[]; + getSessions(): ISession[]; /** * Get a session by its resource URI. */ - getSession(resource: URI): ISessionData | undefined; + getSession(resource: URI): ISession | undefined; /** * Get all session types from all registered providers. @@ -96,9 +95,9 @@ export interface ISessionsManagementService { // -- Active Session -- /** - * Observable for the currently active session as {@link ISessionData}. + * Observable for the currently active session as {@link ISession}. */ - readonly activeSession: IObservable; + readonly activeSession: IObservable; /** * Observable for the currently active sessions provider ID. @@ -138,36 +137,30 @@ export interface ISessionsManagementService { * Create a new session for the given workspace. * Delegates to the provider identified by providerId. */ - createNewSession(providerId: string, workspace: ISessionWorkspace): ISessionData; + createNewSession(providerId: string, workspace: ISessionWorkspace): ISession; /** * Send a request to an existing session */ - sendAndCreateChat(options: ISendRequestOptions, session: ISessionData): Promise; + sendAndCreateChat(options: ISendRequestOptions, session: ISession): Promise; /** * Send the initial request for a session. */ - sendRequest(chat: IChatData, options: ISendRequestOptions, session?: ISessionData): Promise; + sendRequest(chat: IChat, options: ISendRequestOptions, session?: ISession): Promise; /** * Update the session type for a new session. * The provider may recreate the session object. * If the session is the active session, the active session data is updated. */ - setSessionType(chat: IChatData, type: ISessionType): Promise; - - /** - * Commit files in a worktree and refresh the agent sessions model - * so the Changes view reflects the update. - */ - commitWorktreeFiles(session: ISessionData, fileUris: URI[]): Promise; + setSessionType(chat: IChat, type: ISessionType): Promise; /** * Derive a GitHub context (owner, repo, prNumber) from an active session. * Returns `undefined` if the session is not associated with a GitHub repository. */ - getGitHubContext(session: ISessionData): IGitHubSessionContext | undefined; + getGitHubContext(session: ISession): IGitHubSessionContext | undefined; /** * Derive a GitHub context from a session resource URI. @@ -183,22 +176,46 @@ export interface ISessionsManagementService { // -- Session Actions -- /** Archive a session. */ - archiveSession(session: ISessionData): Promise; + archiveSession(session: ISession): Promise; /** Unarchive a session. */ - unarchiveSession(session: ISessionData): Promise; + unarchiveSession(session: ISession): Promise; /** Delete a session. */ - deleteSession(session: ISessionData): Promise; + deleteSession(session: ISession): Promise; /** Delete a single chat from a session. */ - deleteChat(chat: IChatData): Promise; + deleteChat(chat: IChat): Promise; /** Rename a chat. */ - renameChat(chat: IChatData, title: string): Promise; + renameChat(chat: IChat, title: string): Promise; /** Mark a session as read or unread. */ - setRead(session: ISessionData, read: boolean): void; + setRead(session: ISession, read: boolean): void; } export const ISessionsManagementService = createDecorator('sessionsManagementService'); -function latestDateAcrossChats(chats: readonly IChatData[], getter: (chat: IChatData) => Date | undefined): Date | undefined { +function toChat(data: ISessionData): IChat { + return { + chatId: data.id, + resource: data.resource, + providerId: data.providerId, + sessionType: data.sessionType, + icon: data.icon, + createdAt: data.createdAt, + workspace: data.workspace, + title: data.title, + updatedAt: data.updatedAt, + status: data.status, + changes: data.changes, + modelId: data.modelId, + mode: data.mode, + loading: data.loading, + isArchived: data.isArchived, + isRead: data.isRead, + description: data.description, + lastTurnEnd: data.lastTurnEnd, + pullRequest: data.pullRequest, + }; +} + +function latestDateAcrossChats(chats: readonly IChat[], getter: (chat: IChat) => Date | undefined): Date | undefined { let latest: Date | undefined; for (const chat of chats) { const d = getter(chat); @@ -209,6 +226,20 @@ function latestDateAcrossChats(chats: readonly IChatData[], getter: (chat: IChat return latest; } +function aggregateStatusAcrossChats(chats: readonly IChat[], reader: IReader): SessionStatus { + for (const c of chats) { + if (c.status.read(reader) === SessionStatus.NeedsInput) { + return SessionStatus.NeedsInput; + } + } + for (const c of chats) { + if (c.status.read(reader) === SessionStatus.InProgress) { + return SessionStatus.InProgress; + } + } + return chats[0].status.read(reader); +} + export class SessionsManagementService extends Disposable implements ISessionsManagementService { declare readonly _serviceBrand: undefined; @@ -221,8 +252,8 @@ export class SessionsManagementService extends Disposable implements ISessionsMa private _sessionTypes: readonly ISessionType[] = []; - private readonly _activeSession = observableValue(this, undefined); - readonly activeSession: IObservable = this._activeSession; + private readonly _activeSession = observableValue(this, undefined); + readonly activeSession: IObservable = this._activeSession; private readonly _activeProviderId = observableValue(this, undefined); readonly activeProviderId: IObservable = this._activeProviderId; private lastSelectedSession: URI | undefined; @@ -231,14 +262,13 @@ export class SessionsManagementService extends Disposable implements ISessionsMa private readonly _activeSessionType: IContextKey; private readonly _isBackgroundProvider: IContextKey; private readonly _groupModel: SessionsGroupModel; - private readonly _sessionDataCache = new Map(); + private readonly _sessionDataCache = new Map(); constructor( @IStorageService private readonly storageService: IStorageService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @ILogService private readonly logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService private readonly commandService: ICommandService, @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @@ -285,13 +315,13 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } // Find the chat data matching this session resource - const chat = this._getChat(sessionResource); + const chat = this._getSessionData(sessionResource); if (!chat) { return; } // Update the group model's active chat - this._groupModel.setActiveChatId(chat.chatId); + this._groupModel.setActiveChatId(chat.id); } private _initActiveProvider(): void { @@ -323,15 +353,15 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } /** - * Convert an array of chats into deduplicated sessions using the group model. - * Multiple chats may map to the same session group; this returns one - * {@link ISessionData} per unique group. + * Convert an array of session data into deduplicated sessions using the group model. + * Multiple session data entries may map to the same session group; this returns one + * {@link ISession} per unique group. */ - private _chatsToSessions(chats: readonly IChatData[]): ISessionData[] { + private _sessionDataToSessions(chats: readonly ISessionData[]): ISession[] { const seen = new Set(); - const sessions: ISessionData[] = []; + const sessions: ISession[] = []; for (const chat of chats) { - const groupId = this._groupModel.getSessionIdForChat(chat.chatId) ?? chat.chatId; + const groupId = this._groupModel.getSessionIdForChat(chat.id) ?? chat.id; if (!seen.has(groupId)) { seen.add(groupId); sessions.push(this._chatToSession(chat)); @@ -340,19 +370,19 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return sessions; } - private onDidChangeSessionsFromSessionsProviders(e: IChatChangeEvent): void { + private onDidChangeSessionsFromSessionsProviders(e: ISessionChangeEvent): void { const sessionEvent: ISessionsChangeEvent = { - added: this._chatsToSessions(e.added), - removed: this._chatsToSessions(e.removed), - changed: this._chatsToSessions(e.changed), + added: this._sessionDataToSessions(e.added), + removed: this._sessionDataToSessions(e.removed), + changed: this._sessionDataToSessions(e.changed), }; this._onDidChangeSessions.fire(sessionEvent); const currentActive = this._activeSession.get(); // Remove chats from the group model and clean up session cache for (const removed of e.removed) { - const sessionId = this._groupModel.getSessionIdForChat(removed.chatId); - this._groupModel.removeChat(removed.chatId); + const sessionId = this._groupModel.getSessionIdForChat(removed.id); + this._groupModel.removeChat(removed.id); if (sessionId && this._groupModel.getChatIds(sessionId).length === 0) { this._sessionDataCache.delete(sessionId); } @@ -363,7 +393,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } if (e.removed.length) { - if (e.removed.some(r => currentActive.chats.get().find(c => c.chatId === r.chatId))) { + if (e.removed.some(r => currentActive.chats.get().find(c => c.chatId === r.id))) { // Only open new session view if the group has no remaining chats if (this._groupModel.getChatIds(currentActive.sessionId).length === 0) { this.openNewSessionView(); @@ -373,14 +403,14 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } if (e.changed.length) { - const updated = e.changed.find(s => currentActive.chats.get().find(c => c.chatId === s.chatId)); + const updated = e.changed.find(s => currentActive.chats.get().find(c => c.chatId === s.id)); if (updated?.isArchived.get()) { // Only open new session view if all chats in the group are archived const groupId = this._groupModel.getSessionIdForChat(currentActive.sessionId); const chatIds = groupId ? this._groupModel.getChatIds(groupId) : []; const allChats = this.sessionsProvidersService.getSessions(); const allArchived = chatIds.length === 0 || chatIds.every(id => { - const chat = allChats.find(c => c.chatId === id); + const chat = allChats.find(c => c.id === id); return !chat || chat.isArchived.get(); }); if (allArchived) { @@ -433,21 +463,21 @@ export class SessionsManagementService extends Disposable implements ISessionsMa worktreeBaseBranchProtected]; } - getSessions(): ISessionData[] { + getSessions(): ISession[] { const allChats = this.sessionsProvidersService.getSessions(); - const chatById = new Map(); + const chatById = new Map(); for (const chat of allChats) { - chatById.set(chat.chatId, chat); + chatById.set(chat.id, chat); } - const groupedChats = new Map(); + const groupedChats = new Map(); for (const chat of allChats) { - let groupId = this._groupModel.getSessionIdForChat(chat.chatId); + let groupId = this._groupModel.getSessionIdForChat(chat.id); if (!groupId) { // No group exists — create a single-chat group - groupId = chat.chatId; - this._groupModel.addChat(groupId, chat.chatId); + groupId = chat.id; + this._groupModel.addChat(groupId, chat.id); } if (!groupedChats.has(groupId)) { groupedChats.set(groupId, []); @@ -455,7 +485,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } // Order chats within each group according to the group model - const sessions: ISessionData[] = []; + const sessions: ISession[] = []; for (const [groupId, chats] of groupedChats) { const orderedChatIds = this._groupModel.getChatIds(groupId); for (const chatId of orderedChatIds) { @@ -471,13 +501,13 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return sessions; } - private _getChat(resource: URI): IChatData | undefined { + private _getSessionData(resource: URI): ISessionData | undefined { return this.sessionsProvidersService.getSessions().find(s => this.uriIdentityService.extUri.isEqual(s.resource, resource)); } - getSession(resource: URI): ISessionData | undefined { - const chat = this._getChat(resource); - return chat ? this._chatToSession(chat) : undefined; + getSession(resource: URI): ISession | undefined { + const sessionData = this._getSessionData(resource); + return sessionData ? this._chatToSession(sessionData) : undefined; } getAllSessionTypes(): ISessionType[] { @@ -510,7 +540,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa async openChat(chatResource: URI): Promise { const sessionData = this.getSession(chatResource); - const chat = this._getChat(chatResource); + const chat = this._getSessionData(chatResource); this.logService.info(`[SessionsManagement] openChat: ${chatResource.toString()} provider=${chat?.providerId}`); this.isNewChatSessionContext.set(false); @@ -533,7 +563,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa await this.chatWidgetService.openSession(activeChatResource, ChatViewPaneTarget, { preserveFocus: options?.preserveFocus }); } - createNewSession(providerId: string, workspace: ISessionWorkspace): ISessionData { + createNewSession(providerId: string, workspace: ISessionWorkspace): ISession { if (!this.isNewChatSessionContext.get()) { this.isNewChatSessionContext.set(true); } @@ -550,7 +580,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return sessionData; } - async setSessionType(chat: IChatData, type: ISessionType): Promise { + async setSessionType(chat: IChat, type: ISessionType): Promise { const provider = this.sessionsProvidersService.getProviders().find(p => p.id === chat.providerId); if (!provider) { throw new Error(`Sessions provider '${chat.providerId}' not found`); @@ -565,7 +595,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } - async sendAndCreateChat(options: ISendRequestOptions, session: ISessionData): Promise { + async sendAndCreateChat(options: ISendRequestOptions, session: ISession): Promise { const provider = this.sessionsProvidersService.getProviders().find(p => p.id === session.providerId); if (!provider) { throw new Error(`Sessions provider '${session.providerId}' not found`); @@ -573,23 +603,23 @@ export class SessionsManagementService extends Disposable implements ISessionsMa const chatData = provider.createNewSessionFrom(session.chats.get()[0].chatId); - const newChat = await provider.sendRequest(chatData.chatId, options); + const newChat = await provider.sendRequest(chatData.id, options); // Set the new agent session as active if (newChat) { // It's likely that the provider has already added the new chat to the group before provider.sendRequest returns. // This will cause a new group to be created for the new chat which actually belongs to the same session. - if (this._groupModel.hasGroupForSession(newChat.chatId)) { - this._groupModel.deleteSession(newChat.chatId); + if (this._groupModel.hasGroupForSession(newChat.id)) { + this._groupModel.deleteSession(newChat.id); } // Add the new chat to the session's group - this._groupModel.addChat(session.sessionId, newChat.chatId); + this._groupModel.addChat(session.sessionId, newChat.id); } this._onDidChangeSessions.fire({ added: [], removed: [], changed: [session] }); } - async sendRequest(chat: IChatData, options: ISendRequestOptions): Promise { + async sendRequest(chat: IChat, options: ISendRequestOptions): Promise { this.isNewChatSessionContext.set(false); const provider = this.sessionsProvidersService.getProviders().find(p => p.id === chat.providerId); @@ -603,7 +633,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa // Set the new agent session as active if (newChat) { // Add the new chat to the session's group - this._groupModel.addChat(newChat.chatId, newChat.chatId); + this._groupModel.addChat(newChat.id, newChat.id); this.setActiveSession(this._chatToSession(newChat)); } } @@ -622,7 +652,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return repositoryUri; } - private setActiveSession(session: ISessionData | undefined): void { + private setActiveSession(session: ISession | undefined): void { // Update context keys from session data this._activeSessionProviderId.set(session?.providerId ?? ''); this._activeSessionType.set(session?.sessionType ?? ''); @@ -641,21 +671,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this._activeSession.set(session, undefined); } - async commitWorktreeFiles(session: ISessionData, fileUris: URI[]): Promise { - const worktreeUri = session.workspace.get()?.repositories[0]?.workingDirectory; - if (!worktreeUri) { - throw new Error('Cannot commit worktree files: active session has no associated worktree'); - } - for (const fileUri of fileUris) { - await this.commandService.executeCommand( - 'github.copilot.cli.sessions.commitToWorktree', - { worktreeUri, fileUri } - ); - } - await this.agentSessionsService.model.resolve(AgentSessionProviders.Background); - } - - getGitHubContext(session: ISessionData): IGitHubSessionContext | undefined { + getGitHubContext(session: ISession): IGitHubSessionContext | undefined { // 1. Try parsing a github-remote-file URI (Cloud sessions) const repoUri = session.workspace.get()?.repositories[0]?.uri; if (repoUri && repoUri.scheme === GITHUB_REMOTE_FILE_SCHEME) { @@ -701,7 +717,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } getGitHubContextForSession(sessionResource: URI): IGitHubSessionContext | undefined { - // Try finding the ISessionData first (preferred path) + // Try finding the ISession first (preferred path) const sessionData = this.getSession(sessionResource); if (sessionData) { return this.getGitHubContext(sessionData); @@ -735,7 +751,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return URI.joinPath(baseUri, relativePath); } - private _parsePRNumberFromSession(session: ISessionData): number | undefined { + private _parsePRNumberFromSession(session: ISession): number | undefined { const prUri = session.pullRequest.get()?.uri; if (prUri) { const match = /\/pull\/(\d+)/.exec(prUri.path); @@ -747,15 +763,15 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } /** - * Wraps a primary {@link IChatData} and its sibling chats into an {@link ISessionData}. - * Uses `Object.create` so that all properties of the primary chat are inherited + * Wraps a primary {@link ISessionData} and its sibling sessions into an {@link ISession}. + * Uses `Object.create` so that all properties of the primary session are inherited * through the prototype chain, avoiding issues with class getters. * * The `chats` and `activeChat` observables are derived from the group model * and update automatically when the group model fires a change event. */ - private _chatToSession(chat: IChatData): ISessionData { - const sessionId = this._groupModel.getSessionIdForChat(chat.chatId) ?? chat.chatId; + private _chatToSession(chat: ISessionData): ISession { + const sessionId = this._groupModel.getSessionIdForChat(chat.id) ?? chat.id; /* const cached = this._sessionDataCache.get(sessionId); if (cached) { @@ -768,22 +784,22 @@ export class SessionsManagementService extends Disposable implements ISessionsMa () => { const chatIds = this._groupModel.getChatIds(sessionId); if (chatIds.length === 0) { - return [chat]; + return [toChat(chat)]; } const provider = this.sessionsProvidersService.getProviders().find(p => p.id === chat.providerId); const providerChats = provider?.getSessions() || []; - const chatById = new Map(providerChats.map(c => [c.chatId, c])); + const chatById = new Map(providerChats.map(c => [c.id, c])); const chatOrder = new Map(chatIds.map((id, index) => [id, index])); - const resolved = chatIds.map(id => chatById.get(id)).filter((c): c is IChatData => !!c); + const resolved = chatIds.map(id => chatById.get(id)).filter((c): c is ISessionData => !!c); if (resolved.length === 0) { - return [chat]; + return [toChat(chat)]; } - return resolved.sort((a, b) => (chatOrder.get(a.chatId) ?? Infinity) - (chatOrder.get(b.chatId) ?? Infinity)); + return resolved.sort((a, b) => (chatOrder.get(a.id) ?? Infinity) - (chatOrder.get(b.id) ?? Infinity)).map(toChat); }, ); const activeChatObs = chatsObs.map(chats => { if (!this._groupModel.hasGroupForSession(sessionId)) { - return chat; //new Sessions might not be in the group model + return toChat(chat); //new Sessions might not be in the group model } const activeChatId = this._groupModel.getActiveChatId(sessionId); const activeChat = chats.find(c => c.chatId === activeChatId); @@ -793,15 +809,19 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return activeChat; }); - const updatedAtObs = chatsObs.map(chats => latestDateAcrossChats(chats, c => c.updatedAt.get())!); - const lastTurnEndObs = chatsObs.map(chats => latestDateAcrossChats(chats, c => c.lastTurnEnd.get())); + const updatedAtObs = chatsObs.map((chats, reader) => latestDateAcrossChats(chats, c => c.updatedAt.read(reader))!); + const lastTurnEndObs = chatsObs.map((chats, reader) => latestDateAcrossChats(chats, c => c.lastTurnEnd.read(reader))); + const statusObs = chatsObs.map((chats, reader) => aggregateStatusAcrossChats(chats, reader)); + const isReadObs = chatsObs.map((chats, reader) => chats.every(c => c.isRead.read(reader))); const mainChat = chatsObs.get()[0]; - const sessionData: ISessionData = { + const sessionData: ISession = { ...mainChat, // Inherit properties from the primary chat sessionId, + status: statusObs, updatedAt: updatedAtObs, lastTurnEnd: lastTurnEndObs, + isRead: isReadObs, chats: chatsObs, activeChat: activeChatObs, mainChat, @@ -831,19 +851,19 @@ export class SessionsManagementService extends Disposable implements ISessionsMa // -- Session Actions -- - async archiveSession(session: ISessionData): Promise { + async archiveSession(session: ISession): Promise { for (const chat of session.chats.get()) { await this.sessionsProvidersService.archiveSession(chat.chatId); } } - async unarchiveSession(session: ISessionData): Promise { + async unarchiveSession(session: ISession): Promise { for (const chat of session.chats.get()) { await this.sessionsProvidersService.unarchiveSession(chat.chatId); } } - async deleteSession(session: ISessionData): Promise { + async deleteSession(session: ISession): Promise { this._sessionDataCache.delete(session.sessionId); for (const chat of session.chats.get()) { // Clear the chat widget before removing from storage @@ -852,7 +872,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } - async deleteChat(chat: IChatData): Promise { + async deleteChat(chat: IChat): Promise { const session = this.getSession(chat.resource); if (!session) { throw new Error(`Session for chat ${chat.chatId} not found`); @@ -867,11 +887,11 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } } - async renameChat(chat: IChatData, title: string): Promise { + async renameChat(chat: IChat, title: string): Promise { await this.sessionsProvidersService.renameSession(chat.chatId, title); } - setRead(session: ISessionData, read: boolean): void { + setRead(session: ISession, read: boolean): void { for (const chat of session.chats.get()) { this.sessionsProvidersService.setRead(chat.chatId, read); } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsProvider.ts b/src/vs/sessions/contrib/sessions/browser/sessionsProvider.ts index ea77414dcef..33ce6c53d92 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsProvider.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsProvider.ts @@ -6,7 +6,7 @@ import { Event } from '../../../../base/common/event.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; -import { IChatData, ISessionWorkspace } from '../common/sessionData.js'; +import { ISessionData, ISessionWorkspace } from '../common/sessionData.js'; import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; /** @@ -40,10 +40,10 @@ export interface ISessionsBrowseAction { /** * Event fired when sessions change within a provider. */ -export interface IChatChangeEvent { - readonly added: readonly IChatData[]; - readonly removed: readonly IChatData[]; - readonly changed: readonly IChatData[]; +export interface ISessionChangeEvent { + readonly added: readonly ISessionData[]; + readonly removed: readonly ISessionData[]; + readonly changed: readonly ISessionData[]; } /** @@ -83,20 +83,20 @@ export interface ISessionsProvider { // -- Sessions (existing) -- /** Returns all chats owned by this provider. */ - getSessions(): IChatData[]; + getSessions(): ISessionData[]; /** Fires when chats are added, removed, or changed. */ - readonly onDidChangeSessions: Event; + readonly onDidChangeSessions: Event; // -- Session Management -- /** Create a new session for the given workspace. */ - createNewSession(workspace: ISessionWorkspace): IChatData; + createNewSession(workspace: ISessionWorkspace): ISessionData; - createNewSessionFrom(chatId: string): IChatData; + createNewSessionFrom(chatId: string): ISessionData; /** Update the session type for a session. */ - setSessionType(chatId: string, type: ISessionType): IChatData; + setSessionType(chatId: string, type: ISessionType): ISessionData; /** Returns session types available for the given session. */ - getSessionTypes(chat: IChatData): ISessionType[]; + getSessionTypes(session: ISessionData): ISessionType[]; /** Rename a session. */ renameSession(chatId: string, title: string): Promise; /** Set the model for a session. */ @@ -110,8 +110,13 @@ export interface ISessionsProvider { /** Mark a session as read or unread. */ setRead(chatId: string, read: boolean): void; + // -- Untitled -- + + /** Returns the current untitled (not yet sent) session, if any. */ + getUntitledSession(): ISessionData | undefined; // TODO: Shoulds ideally be removed when new chat and picker is cleaned up + // -- Send -- /** Send the initial request for a new session. Returns the created chat data. */ - sendRequest(chatId: string, options: ISendRequestOptions): Promise; + sendRequest(chatId: string, options: ISendRequestOptions): Promise; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsProvidersService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsProvidersService.ts index 980285e25e6..93c9645a50f 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsProvidersService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsProvidersService.ts @@ -7,8 +7,8 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IChatData, ISessionWorkspace } from '../common/sessionData.js'; -import { IChatChangeEvent, ISessionsProvider, ISessionType } from './sessionsProvider.js'; +import { ISessionData, ISessionWorkspace } from '../common/sessionData.js'; +import { ISessionChangeEvent, ISessionsProvider, ISessionType } from './sessionsProvider.js'; import { URI } from '../../../../base/common/uri.js'; export const ISessionsProvidersService = createDecorator('sessionsProvidersService'); @@ -35,16 +35,16 @@ export interface ISessionsProvidersService { /** Get available session types for a provider. */ getSessionTypesForProvider(providerId: string): ISessionType[]; /** Get available session types for a session from its provider. */ - getSessionTypes(session: IChatData): ISessionType[]; + getSessionTypes(session: ISessionData): ISessionType[]; // -- Aggregated Sessions -- /** Get all chats from all providers. */ - getSessions(): IChatData[]; + getSessions(): ISessionData[]; /** Get a chat by its globally unique ID. */ - getSession(chatId: string): IChatData | undefined; + getSession(chatId: string): ISessionData | undefined; /** Fires when sessions change across any provider. */ - readonly onDidChangeSessions: Event; + readonly onDidChangeSessions: Event; // -- Session Actions (routed to the correct provider via sessionId) -- @@ -60,6 +60,8 @@ export interface ISessionsProvidersService { setRead(sessionId: string, read: boolean): void; /** Resolve a repository URI to a session workspace using the given provider. */ resolveWorkspace(providerId: string, repositoryUri: URI): ISessionWorkspace | undefined; + /** Returns the current untitled session for the given provider, if any. */ + getUntitledSession(providerId: string): ISessionData | undefined; // TODO: Shoulds ideally be removed when new chat and picker is cleaned up } /** @@ -75,8 +77,8 @@ export class SessionsProvidersService extends Disposable implements ISessionsPro private readonly _onDidChangeProviders = this._register(new Emitter()); readonly onDidChangeProviders: Event = this._onDidChangeProviders.event; - private readonly _onDidChangeSessions = this._register(new Emitter()); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + private readonly _onDidChangeSessions = this._register(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; // -- Provider Registry -- @@ -119,7 +121,7 @@ export class SessionsProvidersService extends Disposable implements ISessionsPro return [...entry.provider.sessionTypes]; } - getSessionTypes(session: IChatData): ISessionType[] { + getSessionTypes(session: ISessionData): ISessionType[] { const entry = this._providers.get(session.providerId); if (!entry) { return []; @@ -129,20 +131,20 @@ export class SessionsProvidersService extends Disposable implements ISessionsPro // -- Aggregated Sessions -- - getSessions(): IChatData[] { - const sessions: IChatData[] = []; + getSessions(): ISessionData[] { + const sessions: ISessionData[] = []; for (const { provider } of this._providers.values()) { sessions.push(...provider.getSessions()); } return sessions; } - getSession(chatId: string): IChatData | undefined { + getSession(chatId: string): ISessionData | undefined { const { provider } = this._resolveProvider(chatId); if (!provider) { return undefined; } - return provider.getSessions().find(s => s.chatId === chatId); + return provider.getSessions().find(s => s.id === chatId); } // -- Session Actions -- @@ -187,6 +189,11 @@ export class SessionsProvidersService extends Disposable implements ISessionsPro return entry?.provider.resolveWorkspace(repositoryUri); } + getUntitledSession(providerId: string): ISessionData | undefined { + const entry = this._providers.get(providerId); + return entry?.provider.getUntitledSession(); + } + // -- Private Helpers -- /** diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index a78bfe8bc13..b258a996361 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -30,7 +30,7 @@ import { WorkbenchObjectTree } from '../../../../../platform/list/browser/listSe import { IStyleOverride, defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { GITHUB_REMOTE_FILE_SCHEME, ISessionData, ISessionWorkspace, SessionStatus } from '../../common/sessionData.js'; +import { GITHUB_REMOTE_FILE_SCHEME, ISession, ISessionWorkspace, SessionStatus } from '../../common/sessionData.js'; import { ISessionsManagementService } from '../sessionsManagementService.js'; import { AgentSessionApprovalModel, IAgentSessionApprovalInfo } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; @@ -63,7 +63,7 @@ export enum SessionsSorting { export interface ISessionSection { readonly id: string; readonly label: string; - readonly sessions: ISessionData[]; + readonly sessions: ISession[]; } export interface ISessionShowMore { @@ -72,7 +72,7 @@ export interface ISessionShowMore { readonly remainingCount: number; } -export type SessionListItem = ISessionData | ISessionSection | ISessionShowMore; +export type SessionListItem = ISession | ISessionSection | ISessionShowMore; function isSessionSection(item: SessionListItem): item is ISessionSection { return 'sessions' in item && Array.isArray((item as ISessionSection).sessions); @@ -103,7 +103,7 @@ class SessionsTreeDelegate implements IListVirtualDelegate { let height = SessionsTreeDelegate.ITEM_HEIGHT; if (this._approvalModel) { - const approval = getFirstApprovalAcrossChats(this._approvalModel, element as ISessionData, undefined); + const approval = getFirstApprovalAcrossChats(this._approvalModel, element as ISession, undefined); if (approval) { height += SessionItemRenderer.getApprovalRowHeight(approval.label); } @@ -157,11 +157,11 @@ class SessionItemRenderer implements ITreeRenderer(); - readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; + private readonly _onDidChangeItemHeight = new Emitter(); + readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; constructor( - private readonly options: { grouping: () => SessionsGrouping; sorting: () => SessionsSorting; isPinned: (session: ISessionData) => boolean }, + private readonly options: { grouping: () => SessionsGrouping; sorting: () => SessionsSorting; isPinned: (session: ISession) => boolean }, private readonly approvalModel: AgentSessionApprovalModel | undefined, private readonly instantiationService: IInstantiationService, private readonly contextKeyService: IContextKeyService, @@ -203,7 +203,7 @@ class SessionItemRenderer implements ITreeRenderer; - private sessions: ISessionData[] = []; + private sessions: ISession[] = []; private visible = true; private readonly _pinnedSessionIds: Set; private readonly excludedSessionTypes: Set; @@ -791,9 +791,9 @@ export class SessionsList extends Disposable implements ISessionsList { const sorted = this.sortSessions(filtered); // Separate pinned and archived sessions (archived always wins over pinned) - const pinned: ISessionData[] = []; - const archived: ISessionData[] = []; - const regular: ISessionData[] = []; + const pinned: ISession[] = []; + const archived: ISession[] = []; + const regular: ISession[] = []; for (const session of sorted) { if (session.isArchived.get()) { archived.push(session); @@ -950,19 +950,19 @@ export class SessionsList extends Disposable implements ISessionsList { // -- Pinning -- - pinSession(session: ISessionData): void { + pinSession(session: ISession): void { this._pinnedSessionIds.add(session.sessionId); this.savePinnedSessions(); this.update(); } - unpinSession(session: ISessionData): void { + unpinSession(session: ISession): void { this._pinnedSessionIds.delete(session.sessionId); this.savePinnedSessions(); this.update(); } - isSessionPinned(session: ISessionData): boolean { + isSessionPinned(session: ISession): boolean { return this._pinnedSessionIds.has(session.sessionId); } @@ -1155,17 +1155,17 @@ export class SessionsList extends Disposable implements ISessionsList { // -- Sorting -- - private sortSessions(sessions: ISessionData[]): ISessionData[] { + private sortSessions(sessions: ISession[]): ISession[] { return sortSessions(sessions, this.options.sorting()); } // -- Grouping -- - private groupByWorkspace(sessions: ISessionData[]): ISessionSection[] { + private groupByWorkspace(sessions: ISession[]): ISessionSection[] { return groupByWorkspace(sessions); } - private groupByDate(sessions: ISessionData[]): ISessionSection[] { + private groupByDate(sessions: ISession[]): ISessionSection[] { return groupByDate(sessions, this.options.sorting()); } } @@ -1174,7 +1174,7 @@ export class SessionsList extends Disposable implements ISessionsList { //#region Approval Helpers -function getFirstApprovalAcrossChats(approvalModel: AgentSessionApprovalModel, session: ISessionData, reader: IReader | undefined,): IAgentSessionApprovalInfo | undefined { +function getFirstApprovalAcrossChats(approvalModel: AgentSessionApprovalModel, session: ISession, reader: IReader | undefined,): IAgentSessionApprovalInfo | undefined { let oldest: IAgentSessionApprovalInfo | undefined; for (const chat of session.chats.read(reader)) { const approval = approvalModel.getApproval(chat.resource).read(reader); @@ -1189,7 +1189,7 @@ function getFirstApprovalAcrossChats(approvalModel: AgentSessionApprovalModel, s //#region Sorting & Grouping Helpers -export function sortSessions(sessions: ISessionData[], sorting: SessionsSorting): ISessionData[] { +export function sortSessions(sessions: ISession[], sorting: SessionsSorting): ISession[] { return [...sessions].sort((a, b) => { if (sorting === SessionsSorting.Updated) { return b.updatedAt.get().getTime() - a.updatedAt.get().getTime(); @@ -1198,8 +1198,8 @@ export function sortSessions(sessions: ISessionData[], sorting: SessionsSorting) }); } -export function groupByWorkspace(sessions: ISessionData[]): ISessionSection[] { - const groups = new Map(); +export function groupByWorkspace(sessions: ISession[]): ISessionSection[] { + const groups = new Map(); for (const session of sessions) { const workspace = session.workspace.get(); const label = workspace?.label ?? localize('unknown', "Unknown"); @@ -1231,16 +1231,16 @@ export function groupByWorkspace(sessions: ISessionData[]): ISessionSection[] { return result; } -export function groupByDate(sessions: ISessionData[], sorting: SessionsSorting): ISessionSection[] { +export function groupByDate(sessions: ISession[], sorting: SessionsSorting): ISessionSection[] { const now = new Date(); const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const startOfYesterday = startOfToday - 86_400_000; const startOfWeek = startOfToday - 7 * 86_400_000; - const today: ISessionData[] = []; - const yesterday: ISessionData[] = []; - const week: ISessionData[] = []; - const older: ISessionData[] = []; + const today: ISession[] = []; + const yesterday: ISession[] = []; + const week: ISession[] = []; + const older: ISession[] = []; for (const session of sessions) { const time = sorting === SessionsSorting.Updated @@ -1259,7 +1259,7 @@ export function groupByDate(sessions: ISessionData[], sorting: SessionsSorting): } const sections: ISessionSection[] = []; - const addGroup = (id: string, label: string, groupSessions: ISessionData[]) => { + const addGroup = (id: string, label: string, groupSessions: ISession[]) => { if (groupSessions.length > 0) { sections.push({ id, label, sessions: groupSessions }); } diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts index e13f1fa47ec..c733b4c8341 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts @@ -20,7 +20,7 @@ import { AUX_WINDOW_GROUP } from '../../../../../workbench/services/editor/commo import { SessionsCategories } from '../../../../common/categories.js'; import { SessionItemToolbarMenuId, SessionItemContextMenuId, SessionSectionToolbarMenuId, SessionSectionTypeContext, IsSessionPinnedContext, IsSessionArchivedContext, IsSessionReadContext, SessionsGrouping, SessionsSorting, ISessionSection } from './sessionsList.js'; import { ISessionsManagementService, IsNewChatSessionContext } from '../sessionsManagementService.js'; -import { ISessionData, SessionStatus } from '../../common/sessionData.js'; +import { ISession, SessionStatus } from '../../common/sessionData.js'; import { IsWorkspaceGroupCappedContext, SessionsViewFilterOptionsSubMenu, SessionsViewFilterSubMenu, SessionsViewGroupingContext, SessionsViewId, SessionsView, SessionsViewSortingContext } from './sessionsView.js'; import { SessionsViewId as NewChatViewId, NewChatViewPane } from '../../../chat/browser/newChatViewPane.js'; import { Menus } from '../../../../browser/menus.js'; @@ -400,7 +400,7 @@ registerAction2(class PinSessionAction extends Action2 { }] }); } - run(accessor: ServicesAccessor, context?: ISessionData): void { + run(accessor: ServicesAccessor, context?: ISession): void { if (!context) { return; } @@ -435,7 +435,7 @@ registerAction2(class UnpinSessionAction extends Action2 { }] }); } - run(accessor: ServicesAccessor, context?: ISessionData): void { + run(accessor: ServicesAccessor, context?: ISession): void { if (!context) { return; } @@ -464,7 +464,7 @@ registerAction2(class ArchiveSessionAction extends Action2 { }] }); } - async run(accessor: ServicesAccessor, context?: ISessionData): Promise { + async run(accessor: ServicesAccessor, context?: ISession): Promise { if (!context) { return; } @@ -492,7 +492,7 @@ registerAction2(class UnarchiveSessionAction extends Action2 { }] }); } - async run(accessor: ServicesAccessor, context?: ISessionData): Promise { + async run(accessor: ServicesAccessor, context?: ISession): Promise { if (!context) { return; } @@ -517,7 +517,7 @@ registerAction2(class MarkSessionReadAction extends Action2 { }] }); } - run(accessor: ServicesAccessor, context?: ISessionData): void { + run(accessor: ServicesAccessor, context?: ISession): void { if (!context) { return; } @@ -542,7 +542,7 @@ registerAction2(class MarkSessionUnreadAction extends Action2 { }] }); } - run(accessor: ServicesAccessor, context?: ISessionData): void { + run(accessor: ServicesAccessor, context?: ISession): void { if (!context) { return; } @@ -563,7 +563,7 @@ registerAction2(class OpenSessionInNewWindowAction extends Action2 { }] }); } - async run(accessor: ServicesAccessor, context?: ISessionData): Promise { + async run(accessor: ServicesAccessor, context?: ISession): Promise { if (!context) { return; } diff --git a/src/vs/sessions/contrib/sessions/common/sessionData.ts b/src/vs/sessions/contrib/sessions/common/sessionData.ts index c14004a0c18..b2e164d5191 100644 --- a/src/vs/sessions/contrib/sessions/common/sessionData.ts +++ b/src/vs/sessions/contrib/sessions/common/sessionData.ts @@ -67,11 +67,59 @@ export interface ISessionPullRequest { } /** - * A single chat as exposed by sessions providers. + * A single session as exposed by sessions providers. * Self-contained facade — components should not reach back to underlying * services to resolve additional data. */ -export interface IChatData { +export interface ISessionData { + /** Globally unique session ID (`providerId:localId`). */ + readonly id: string; + /** Resource URI identifying this session. */ + readonly resource: URI; + /** ID of the provider that owns this session. */ + readonly providerId: string; + /** Session type ID (e.g., 'copilot-cli', 'copilot-cloud'). */ + readonly sessionType: string; + /** Icon for this session. */ + readonly icon: ThemeIcon; + /** When the session was created. */ + readonly createdAt: Date; + /** Workspace this session operates on. */ + readonly workspace: IObservable; + + // Reactive properties + + /** Session display title (changes when auto-titled or renamed). */ + readonly title: IObservable; + /** When the session was last updated. */ + readonly updatedAt: IObservable; + /** Current session status. */ + readonly status: IObservable; + /** File changes produced by the session. */ + readonly changes: IObservable; + /** Currently selected model identifier. */ + readonly modelId: IObservable; + /** Currently selected mode identifier and kind. */ + readonly mode: IObservable<{ readonly id: string; readonly kind: string } | undefined>; + /** Whether the session is still initializing (e.g., resolving git repository). */ + readonly loading: IObservable; + /** Whether the session is archived. */ + readonly isArchived: IObservable; + /** Whether the session has been read. */ + readonly isRead: IObservable; + /** Status description shown while the session is active (e.g., current agent action). */ + readonly description: IObservable; + /** Timestamp of when the last agent turn ended, if any. */ + readonly lastTurnEnd: IObservable; + /** Pull request associated with this session, if any. */ + readonly pullRequest: IObservable; +} + +/** + * A single chat within a session, produced by the sessions management layer. + * Has the same shape as {@link ISessionData} but uses `chatId` as its identifier. + */ +export interface IChat { /** Globally unique chat ID (`providerId:localId`). */ readonly chatId: string; /** Resource URI identifying this chat. */ @@ -117,9 +165,9 @@ export interface IChatData { /** * A session groups one or more chats together. - * All {@link IChatData} fields are propagated from the primary (first) chat. + * All {@link ISessionData} fields are propagated from the primary (first) chat. */ -export interface ISessionData { +export interface ISession { /** Globally unique session ID (`providerId:localId`). */ readonly sessionId: string; /** Resource URI identifying this session. */ @@ -162,9 +210,9 @@ export interface ISessionData { /** Pull request associated with this session, if any. */ readonly pullRequest: IObservable; /** The chats belonging to this session group. */ - readonly chats: IObservable; + readonly chats: IObservable; /** The currently active chat within this session group. */ - readonly activeChat: IObservable; + readonly activeChat: IObservable; /** The main chat within this session group (the first chat of the session). */ - readonly mainChat: IChatData; + readonly mainChat: IChat; } diff --git a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts index 40f6cbce356..9cf562073d8 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts @@ -24,7 +24,7 @@ import { ComponentFixtureContext, createEditorServices, defineComponentFixture, import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js'; import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem } from '../../browser/customizationsToolbar.contribution.js'; import { ISessionsManagementService } from '../../browser/sessionsManagementService.js'; -import { ISessionData } from '../../common/sessionData.js'; +import { ISession } from '../../common/sessionData.js'; import { Menus } from '../../../../browser/menus.js'; // Ensure color registrations are loaded @@ -201,7 +201,7 @@ function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?: override readonly onDidChangeLanguageModels = Event.None; }()); reg.defineInstance(ISessionsManagementService, new class extends mock() { - override readonly activeSession = observableValue('activeSession', undefined); + override readonly activeSession = observableValue('activeSession', undefined); }()); reg.defineInstance(IFileService, new class extends mock() { override readonly onDidFilesChange = Event.None; diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts index 473c3b272c7..f29dab46b0e 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts @@ -8,7 +8,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IChatData, ISessionData, SessionStatus } from '../../common/sessionData.js'; +import { IChat, ISession, SessionStatus } from '../../common/sessionData.js'; import { groupByWorkspace, sortSessions, SessionsSorting } from '../../browser/views/sessionsList.js'; function createSession(id: string, opts: { @@ -16,7 +16,7 @@ function createSession(id: string, opts: { createdAt?: Date; updatedAt?: Date; isArchived?: boolean; -}): ISessionData { +}): ISession { const createdAt = opts.createdAt ?? new Date(); const updatedAt = opts.updatedAt ?? createdAt; return { @@ -44,8 +44,8 @@ function createSession(id: string, opts: { description: observableValue(`description-${id}`, undefined), lastTurnEnd: observableValue(`lastTurnEnd-${id}`, undefined), pullRequest: observableValue(`pullRequest-${id}`, undefined), - chats: observableValue(`chats-${id}`, []), - activeChat: observableValue(`activeChat-${id}`, undefined!), + chats: observableValue(`chats-${id}`, []), + activeChat: observableValue(`activeChat-${id}`, undefined!), mainChat: undefined!, }; } diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index d248d4c7005..c1ced270dc7 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -18,7 +18,7 @@ import { TerminalCapability } from '../../../../platform/terminal/common/capabil import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { Menus } from '../../../browser/menus.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -import { ISessionData } from '../../sessions/common/sessionData.js'; +import { ISession } from '../../sessions/common/sessionData.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; @@ -33,7 +33,7 @@ const SessionsTerminalViewVisibleContext = new RawContextKey('sessionsT * background sessions only. Returns `undefined` for non-background sessions * (Cloud, Local, etc.) which have no local worktree, or when no path is available. */ -function getSessionCwd(session: ISessionData | undefined): URI | undefined { +function getSessionCwd(session: ISession | undefined): URI | undefined { if (session?.sessionType !== AgentSessionProviders.Background) { return undefined; } @@ -139,7 +139,7 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben return existing; } - private async _onActiveSessionChanged(session: ISessionData | undefined): Promise { + private async _onActiveSessionChanged(session: ISession | undefined): Promise { if (!session) { return; } diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index 130ec222d04..afd5653f3a5 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -16,7 +16,7 @@ import { ITerminalInstance, ITerminalService } from '../../../../../workbench/co import { ITerminalCapabilityStore, ICommandDetectionCapability, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { AgentSessionProviders } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ISessionsChangeEvent, ISessionsManagementService } from '../../../sessions/browser/sessionsManagementService.js'; -import { IChatData, ISessionData } from '../../../sessions/common/sessionData.js'; +import { IChat, ISession } from '../../../sessions/common/sessionData.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { SessionsTerminalContribution } from '../../browser/sessionsTerminalContribution.js'; import { TestPathService } from '../../../../../workbench/test/browser/workbenchTestServices.js'; @@ -46,7 +46,7 @@ function makeAgentSession(opts: { worktree?: URI; providerType?: string; isArchived?: boolean; -}): ISessionData { +}): ISession { const repo = opts.repository || opts.worktree ? { uri: opts.repository ?? opts.worktree!, workingDirectory: opts.worktree, @@ -54,7 +54,7 @@ function makeAgentSession(opts: { baseBranchName: undefined, baseBranchProtected: undefined, } : undefined; - const chat: IChatData = { + const chat: IChat = { chatId: 'test:session', resource: URI.parse('file:///session'), providerId: 'test', @@ -75,11 +75,11 @@ function makeAgentSession(opts: { description: observableValue('test.description', undefined), pullRequest: observableValue('test.pullRequest', undefined), }; - const session: ISessionData = { ...chat, sessionId: chat.chatId, chats: observableValue('test.chats', [chat]), activeChat: observableValue('test.activeChat', chat), mainChat: chat }; + const session: ISession = { ...chat, sessionId: chat.chatId, chats: observableValue('test.chats', [chat]), activeChat: observableValue('test.activeChat', chat), mainChat: chat }; return session; } -function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerType?: string }): ISessionData { +function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerType?: string }): ISession { const repo = opts.repository || opts.worktree ? { uri: opts.repository ?? opts.worktree!, workingDirectory: opts.worktree, @@ -87,7 +87,7 @@ function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerT baseBranchName: undefined, baseBranchProtected: undefined, } : undefined; - const chat: IChatData = { + const chat: IChat = { chatId: 'test:non-agent', resource: URI.parse('file:///session'), providerId: 'test', @@ -108,7 +108,7 @@ function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerT description: observableValue('test.description', undefined), pullRequest: observableValue('test.pullRequest', undefined), }; - const session: ISessionData = { ...chat, sessionId: chat.chatId, chats: observableValue('test.chats', [chat]), activeChat: observableValue('test.activeChat', chat), mainChat: chat }; + const session: ISession = { ...chat, sessionId: chat.chatId, chats: observableValue('test.chats', [chat]), activeChat: observableValue('test.activeChat', chat), mainChat: chat }; return session; } @@ -148,7 +148,7 @@ function addCommandToInstance(instance: ITerminalInstance, timestamp: number): v suite('SessionsTerminalContribution', () => { const store = new DisposableStore(); let contribution: SessionsTerminalContribution; - let activeSessionObs: ReturnType>; + let activeSessionObs: ReturnType>; let onDidChangeSessions: Emitter; let onDidCreateInstance: Emitter; @@ -179,7 +179,7 @@ suite('SessionsTerminalContribution', () => { const instantiationService = store.add(new TestInstantiationService()); - activeSessionObs = observableValue('activeSession', undefined); + activeSessionObs = observableValue('activeSession', undefined); onDidChangeSessions = store.add(new Emitter()); onDidCreateInstance = store.add(new Emitter()); diff --git a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts index 9f1083cef72..3c47c1c2c4f 100644 --- a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts +++ b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts @@ -17,7 +17,7 @@ import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/co import { getGitHubRemoteFileDisplayName } from '../../fileTreeView/browser/githubFileSystemProvider.js'; import { Queue } from '../../../../base/common/async.js'; import { AGENT_HOST_SCHEME } from '../../../../platform/agentHost/common/agentHostUri.js'; -import { ISessionData } from '../../sessions/common/sessionData.js'; +import { ISession } from '../../sessions/common/sessionData.js'; export class WorkspaceFolderManagementContribution extends Disposable implements IWorkbenchContribution { @@ -39,7 +39,7 @@ export class WorkspaceFolderManagementContribution extends Disposable implements })); } - private async updateWorkspaceFoldersForSession(session: ISessionData | undefined): Promise { + private async updateWorkspaceFoldersForSession(session: ISession | undefined): Promise { await this.manageTrustWorkspaceForSession(session); const activeSessionFolderData = this.getActiveSessionFolderData(session); const currentRepo = this.workspaceContextService.getWorkspace().folders[0]?.uri; @@ -63,7 +63,7 @@ export class WorkspaceFolderManagementContribution extends Disposable implements await this.workspaceEditingService.updateFolders(0, 1, [activeSessionFolderData], true); } - private getActiveSessionFolderData(session: ISessionData | undefined): IWorkspaceFolderCreationData | undefined { + private getActiveSessionFolderData(session: ISession | undefined): IWorkspaceFolderCreationData | undefined { if (!session) { return undefined; } @@ -102,7 +102,7 @@ export class WorkspaceFolderManagementContribution extends Disposable implements return undefined; } - private async manageTrustWorkspaceForSession(session: ISessionData | undefined): Promise { + private async manageTrustWorkspaceForSession(session: ISession | undefined): Promise { if (session?.sessionType !== AgentSessionProviders.Background) { return; }