mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 07:13:45 +01:00
Merge pull request #305730 from microsoft/benibenj/ratty-urial
Session types changes and smarter chat to session mapping
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()];
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 --
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user