Merge pull request #305730 from microsoft/benibenj/ratty-urial

Session types changes and smarter chat to session mapping
This commit is contained in:
Benjamin Christopher Simmonds
2026-03-27 18:10:48 +01:00
committed by GitHub
29 changed files with 489 additions and 439 deletions

View File

@@ -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);
}

View File

@@ -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);
}));
}
}

View File

@@ -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);
}));
}

View File

@@ -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<IRunScriptActionContext | undefined>;
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<void> {
const status = session.status.read(undefined);
if (status === SessionStatus.Untitled) {
const viewPane = that._viewsService.getViewWithId<NewChatViewPane>(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<ITaskEntry | undefined> {
private async _showConfigureQuickPick(session: ISession): Promise<ITaskEntry | undefined> {
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<ITaskEntry | undefined> {
private async _showCustomCommandInput(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add'): Promise<ITaskEntry | undefined> {
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<IRunScriptCustomTaskWidgetResult | undefined> {
private _showCustomCommandWidget(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add'): Promise<IRunScriptCustomTaskWidgetResult | undefined> {
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<IRunScriptActionContext | undefined>,
private readonly _showConfigureQuickPick: (session: ISessionData) => Promise<ITaskEntry | undefined>,
private readonly _showCustomCommandInput: (session: ISessionData, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => Promise<ITaskEntry | undefined>,
private readonly _showConfigureQuickPick: (session: ISession) => Promise<ITaskEntry | undefined>,
private readonly _showCustomCommandInput: (session: ISession, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => Promise<ITaskEntry | undefined>,
@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<NewChatViewPane>(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);
},
});

View File

@@ -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 = [];

View File

@@ -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<readonly ISessionTaskWithTarget[]>;
getSessionTasks(session: ISession): IObservable<readonly ISessionTaskWithTarget[]>;
/**
* Returns tasks that do NOT have `inSessions: true` — used as
* suggestions in the "Add Run Action" picker.
*/
getNonSessionTasks(session: ISessionData): Promise<readonly INonSessionTaskEntry[]>;
getNonSessionTasks(session: ISession): Promise<readonly INonSessionTaskEntry[]>;
/**
* 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<void>;
addTaskToSessions(task: ITaskEntry, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise<void>;
/**
* 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<ITaskEntry | undefined>;
createAndAddTask(label: string | undefined, command: string, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise<ITaskEntry | undefined>;
/**
* 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<void>;
updateTask(originalTaskLabel: string, updatedTask: ITaskEntry, session: ISession, currentTarget: TaskStorageTarget, newTarget: TaskStorageTarget): Promise<void>;
/**
* Removes an existing task entry from its tasks.json.
*/
removeTask(taskLabel: string, session: ISessionData, target: TaskStorageTarget): Promise<void>;
removeTask(taskLabel: string, session: ISession, target: TaskStorageTarget): Promise<void>;
/**
* 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<void>;
runTask(task: ITaskEntry, session: ISession): Promise<void>;
/**
* 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<readonly ISessionTaskWithTarget[]> {
getSessionTasks(session: ISession): IObservable<readonly ISessionTaskWithTarget[]> {
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<readonly INonSessionTaskEntry[]> {
async getNonSessionTasks(session: ISession): Promise<readonly INonSessionTaskEntry[]> {
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<void> {
async addTaskToSessions(task: ITaskEntry, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise<void> {
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<ITaskEntry | undefined> {
async createAndAddTask(label: string | undefined, command: string, session: ISession, target: TaskStorageTarget, options?: ITaskRunOptions): Promise<ITaskEntry | undefined> {
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<void> {
async updateTask(originalTaskLabel: string, updatedTask: ITaskEntry, session: ISession, currentTarget: TaskStorageTarget, newTarget: TaskStorageTarget): Promise<void> {
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<void> {
async removeTask(taskLabel: string, session: ISession, target: TaskStorageTarget): Promise<void> {
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<void> {
async runTask(task: ITaskEntry, session: ISession): Promise<void> {
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<void> {
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<string, string> {
const raw = this._storageService.get(SessionsConfigurationService._PINNED_TASK_LABELS_KEY, StorageScope.APPLICATION);
if (raw) {

View File

@@ -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<string, string>;
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<typeof observableValue<ISessionData | undefined>>;
let activeSessionObs: ReturnType<typeof observableValue<ISession | undefined>>;
let tasksByLabel: Map<string, Task>;
let workspaceFoldersByUri: Map<string, IWorkspaceFolder>;
@@ -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<ISessionsManagementService>() {
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 ---

View File

@@ -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) {

View File

@@ -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<ISessionsManagementService>() {
private readonly _onDidChangeSessions: Emitter<ISessionsChangeEvent>;
override readonly onDidChangeSessions: Event<ISessionsChangeEvent>;
override readonly activeSession: IObservable<ISessionData | undefined>;
override readonly activeSession: IObservable<ISession | undefined>;
private readonly _sessions = new Map<string, ISessionData>();
private readonly _sessions = new Map<string, ISession>();
constructor(disposables: DisposableStore) {
super();
this._onDidChangeSessions = disposables.add(new Emitter<ISessionsChangeEvent>());
this.onDidChangeSessions = this._onDidChangeSessions.event;
this.activeSession = observableValue<ISessionData | undefined>('test.activeSession', undefined);
this.activeSession = observableValue<ISession | undefined>('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<readonly IChatSessionFileChange[]>('test.changes',
(changes ?? []).map(c => ({ modifiedUri: c.modifiedUri ?? c.uri, originalUri: c.originalUri, insertions: c.insertions, deletions: c.deletions }))
);
const isArchivedObs = observableValue<boolean>('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()];
}

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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<ChatPermissionLevel>;
readonly branchObservable: IObservable<string | undefined>;
readonly isolationModeObservable: IObservable<string | undefined>;
@@ -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<IChatChangeEvent>());
readonly onDidChangeSessions: Event<IChatChangeEvent> = this._onDidChangeSessions.event;
private readonly _onDidChangeSessions = this._register(new Emitter<ISessionChangeEvent>());
readonly onDidChangeSessions: Event<ISessionChangeEvent> = this._onDidChangeSessions.event;
/** Cache of adapted sessions, keyed by resource URI string. */
private readonly _sessionCache = new Map<string, AgentSessionAdapter>();
@@ -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<IChatData> {
async sendRequest(chatId: string, options: ISendRequestOptions): Promise<ISessionData> {
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<IChatData> {
private async _waitForNewAgentSession(target: AgentSessionTarget, existingSessions: ResourceSet): Promise<ISessionData> {
const found = [...this._sessionCache.values()].find(s => s.sessionType === target && !existingSessions.has(s.resource));
if (found) {
return found;
}
return new Promise<IChatData>(resolve => {
return new Promise<ISessionData>(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<string>();
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));
}

View File

@@ -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<boolean>('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 {

View File

@@ -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);
}
}

View File

@@ -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);
}
}));

View File

@@ -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;
}

View File

@@ -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<IChatChangeEvent>());
readonly onDidChangeSessions: Event<IChatChangeEvent> = this._onDidChangeSessions.event;
private readonly _onDidChangeSessions = this._register(new Emitter<ISessionChangeEvent>());
readonly onDidChangeSessions: Event<ISessionChangeEvent> = 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<IChatData> {
async sendRequest(chatId: string, options: ISendRequestOptions): Promise<ISessionData> {
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<string>();
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<string>): Promise<IChatData | undefined> {
private async _waitForNewSession(existingKeys: Set<string>): Promise<ISessionData | undefined> {
// 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<IChatData | undefined>(resolve => {
return new Promise<ISessionData | undefined>(resolve => {
const listener = this._onDidChangeSessions.event(e => {
const newSession = e.added.find(s => {
const rawId = s.resource.path.substring(1);

View File

@@ -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: {

View File

@@ -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<ISessionData | undefined>;
readonly activeSession: IObservable<ISession | undefined>;
/**
* 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<void>;
sendAndCreateChat(options: ISendRequestOptions, session: ISession): Promise<void>;
/**
* Send the initial request for a session.
*/
sendRequest(chat: IChatData, options: ISendRequestOptions, session?: ISessionData): Promise<void>;
sendRequest(chat: IChat, options: ISendRequestOptions, session?: ISession): Promise<void>;
/**
* 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<void>;
/**
* Commit files in a worktree and refresh the agent sessions model
* so the Changes view reflects the update.
*/
commitWorktreeFiles(session: ISessionData, fileUris: URI[]): Promise<void>;
setSessionType(chat: IChat, type: ISessionType): Promise<void>;
/**
* 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<void>;
archiveSession(session: ISession): Promise<void>;
/** Unarchive a session. */
unarchiveSession(session: ISessionData): Promise<void>;
unarchiveSession(session: ISession): Promise<void>;
/** Delete a session. */
deleteSession(session: ISessionData): Promise<void>;
deleteSession(session: ISession): Promise<void>;
/** Delete a single chat from a session. */
deleteChat(chat: IChatData): Promise<void>;
deleteChat(chat: IChat): Promise<void>;
/** Rename a chat. */
renameChat(chat: IChatData, title: string): Promise<void>;
renameChat(chat: IChat, title: string): Promise<void>;
/** Mark a session as read or unread. */
setRead(session: ISessionData, read: boolean): void;
setRead(session: ISession, read: boolean): void;
}
export const ISessionsManagementService = createDecorator<ISessionsManagementService>('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<ISessionData | undefined>(this, undefined);
readonly activeSession: IObservable<ISessionData | undefined> = this._activeSession;
private readonly _activeSession = observableValue<ISession | undefined>(this, undefined);
readonly activeSession: IObservable<ISession | undefined> = this._activeSession;
private readonly _activeProviderId = observableValue<string | undefined>(this, undefined);
readonly activeProviderId: IObservable<string | undefined> = this._activeProviderId;
private lastSelectedSession: URI | undefined;
@@ -231,14 +262,13 @@ export class SessionsManagementService extends Disposable implements ISessionsMa
private readonly _activeSessionType: IContextKey<string>;
private readonly _isBackgroundProvider: IContextKey<boolean>;
private readonly _groupModel: SessionsGroupModel;
private readonly _sessionDataCache = new Map<string, ISessionData>();
private readonly _sessionDataCache = new Map<string, ISession>();
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<string>();
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<string, IChatData>();
const chatById = new Map<string, ISessionData>();
for (const chat of allChats) {
chatById.set(chat.chatId, chat);
chatById.set(chat.id, chat);
}
const groupedChats = new Map<string, IChatData[]>();
const groupedChats = new Map<string, ISessionData[]>();
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<void> {
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<void> {
async setSessionType(chat: IChat, type: ISessionType): Promise<void> {
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<void> {
async sendAndCreateChat(options: ISendRequestOptions, session: ISession): Promise<void> {
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<void> {
async sendRequest(chat: IChat, options: ISendRequestOptions): Promise<void> {
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<void> {
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<void> {
async archiveSession(session: ISession): Promise<void> {
for (const chat of session.chats.get()) {
await this.sessionsProvidersService.archiveSession(chat.chatId);
}
}
async unarchiveSession(session: ISessionData): Promise<void> {
async unarchiveSession(session: ISession): Promise<void> {
for (const chat of session.chats.get()) {
await this.sessionsProvidersService.unarchiveSession(chat.chatId);
}
}
async deleteSession(session: ISessionData): Promise<void> {
async deleteSession(session: ISession): Promise<void> {
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<void> {
async deleteChat(chat: IChat): Promise<void> {
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<void> {
async renameChat(chat: IChat, title: string): Promise<void> {
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);
}

View File

@@ -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<IChatChangeEvent>;
readonly onDidChangeSessions: Event<ISessionChangeEvent>;
// -- 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<void>;
/** 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<IChatData>;
sendRequest(chatId: string, options: ISendRequestOptions): Promise<ISessionData>;
}

View File

@@ -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<ISessionsProvidersService>('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<IChatChangeEvent>;
readonly onDidChangeSessions: Event<ISessionChangeEvent>;
// -- 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<void>());
readonly onDidChangeProviders: Event<void> = this._onDidChangeProviders.event;
private readonly _onDidChangeSessions = this._register(new Emitter<IChatChangeEvent>());
readonly onDidChangeSessions: Event<IChatChangeEvent> = this._onDidChangeSessions.event;
private readonly _onDidChangeSessions = this._register(new Emitter<ISessionChangeEvent>());
readonly onDidChangeSessions: Event<ISessionChangeEvent> = 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 --
/**

View File

@@ -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<SessionListItem> {
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<SessionListItem, FuzzyScore,
return lineCount * SessionItemRenderer._APPROVAL_ROW_LINE_HEIGHT + SessionItemRenderer._APPROVAL_ROW_OVERHEAD;
}
private readonly _onDidChangeItemHeight = new Emitter<ISessionData>();
readonly onDidChangeItemHeight: Event<ISessionData> = this._onDidChangeItemHeight.event;
private readonly _onDidChangeItemHeight = new Emitter<ISession>();
readonly onDidChangeItemHeight: Event<ISession> = 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<SessionListItem, FuzzyScore,
this.renderSession(element, template);
}
private renderSession(element: ISessionData, template: ISessionItemTemplate): void {
private renderSession(element: ISession, template: ISessionItemTemplate): void {
template.elementDisposables.clear();
// Toolbar context
@@ -360,7 +360,7 @@ class SessionItemRenderer implements ITreeRenderer<SessionListItem, FuzzyScore,
}
}
private renderApprovalRow(element: ISessionData, template: ISessionItemTemplate): void {
private renderApprovalRow(element: ISession, template: ISessionItemTemplate): void {
if (!this.approvalModel) {
return;
}
@@ -592,9 +592,9 @@ export interface ISessionsList {
update(expandAll?: boolean): void;
openFind(): void;
resetSectionCollapseState(): void;
pinSession(session: ISessionData): void;
unpinSession(session: ISessionData): void;
isSessionPinned(session: ISessionData): boolean;
pinSession(session: ISession): void;
unpinSession(session: ISession): void;
isSessionPinned(session: ISession): boolean;
setSessionTypeExcluded(sessionTypeId: string, excluded: boolean): void;
isSessionTypeExcluded(sessionTypeId: string): boolean;
setStatusExcluded(status: SessionStatus, excluded: boolean): void;
@@ -621,7 +621,7 @@ export class SessionsList extends Disposable implements ISessionsList {
private readonly listContainer: HTMLElement;
private readonly tree: WorkbenchObjectTree<SessionListItem, FuzzyScore>;
private sessions: ISessionData[] = [];
private sessions: ISession[] = [];
private visible = true;
private readonly _pinnedSessionIds: Set<string>;
private readonly excludedSessionTypes: Set<string>;
@@ -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<string, ISessionData[]>();
export function groupByWorkspace(sessions: ISession[]): ISessionSection[] {
const groups = new Map<string, ISession[]>();
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 });
}

View File

@@ -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<void> {
async run(accessor: ServicesAccessor, context?: ISession): Promise<void> {
if (!context) {
return;
}
@@ -492,7 +492,7 @@ registerAction2(class UnarchiveSessionAction extends Action2 {
}]
});
}
async run(accessor: ServicesAccessor, context?: ISessionData): Promise<void> {
async run(accessor: ServicesAccessor, context?: ISession): Promise<void> {
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<void> {
async run(accessor: ServicesAccessor, context?: ISession): Promise<void> {
if (!context) {
return;
}

View File

@@ -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<ISessionWorkspace | undefined>;
// Reactive properties
/** Session display title (changes when auto-titled or renamed). */
readonly title: IObservable<string>;
/** When the session was last updated. */
readonly updatedAt: IObservable<Date>;
/** Current session status. */
readonly status: IObservable<SessionStatus>;
/** File changes produced by the session. */
readonly changes: IObservable<readonly IChatSessionFileChange[]>;
/** Currently selected model identifier. */
readonly modelId: IObservable<string | undefined>;
/** 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<boolean>;
/** Whether the session is archived. */
readonly isArchived: IObservable<boolean>;
/** Whether the session has been read. */
readonly isRead: IObservable<boolean>;
/** Status description shown while the session is active (e.g., current agent action). */
readonly description: IObservable<string | undefined>;
/** Timestamp of when the last agent turn ended, if any. */
readonly lastTurnEnd: IObservable<Date | undefined>;
/** Pull request associated with this session, if any. */
readonly pullRequest: IObservable<ISessionPullRequest | undefined>;
}
/**
* 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<ISessionPullRequest | undefined>;
/** The chats belonging to this session group. */
readonly chats: IObservable<readonly IChatData[]>;
readonly chats: IObservable<readonly IChat[]>;
/** The currently active chat within this session group. */
readonly activeChat: IObservable<IChatData>;
readonly activeChat: IObservable<IChat>;
/** The main chat within this session group (the first chat of the session). */
readonly mainChat: IChatData;
readonly mainChat: IChat;
}

View File

@@ -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<ISessionsManagementService>() {
override readonly activeSession = observableValue<ISessionData | undefined>('activeSession', undefined);
override readonly activeSession = observableValue<ISession | undefined>('activeSession', undefined);
}());
reg.defineInstance(IFileService, new class extends mock<IFileService>() {
override readonly onDidFilesChange = Event.None;

View File

@@ -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<readonly IChatData[]>(`chats-${id}`, []),
activeChat: observableValue<IChatData>(`activeChat-${id}`, undefined!),
chats: observableValue<readonly IChat[]>(`chats-${id}`, []),
activeChat: observableValue<IChat>(`activeChat-${id}`, undefined!),
mainChat: undefined!,
};
}

View File

@@ -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<boolean>('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<void> {
private async _onActiveSessionChanged(session: ISession | undefined): Promise<void> {
if (!session) {
return;
}

View File

@@ -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<typeof observableValue<ISessionData | undefined>>;
let activeSessionObs: ReturnType<typeof observableValue<ISession | undefined>>;
let onDidChangeSessions: Emitter<ISessionsChangeEvent>;
let onDidCreateInstance: Emitter<ITerminalInstance>;
@@ -179,7 +179,7 @@ suite('SessionsTerminalContribution', () => {
const instantiationService = store.add(new TestInstantiationService());
activeSessionObs = observableValue<ISessionData | undefined>('activeSession', undefined);
activeSessionObs = observableValue<ISession | undefined>('activeSession', undefined);
onDidChangeSessions = store.add(new Emitter<ISessionsChangeEvent>());
onDidCreateInstance = store.add(new Emitter<ITerminalInstance>());

View File

@@ -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<void> {
private async updateWorkspaceFoldersForSession(session: ISession | undefined): Promise<void> {
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<void> {
private async manageTrustWorkspaceForSession(session: ISession | undefined): Promise<void> {
if (session?.sessionType !== AgentSessionProviders.Background) {
return;
}