Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { getPartByLocation } from '../../../../services/views/browser/viewsServi
import { IWorkbenchLayoutService, Position } from '../../../../services/layout/browser/layoutService.js';
import { IAgentSessionsService } from './agentSessionsService.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
import { ChatEditorInput, showClearEditingSessionConfirmation } from '../widgetHosts/editor/chatEditorInput.js';
import { ChatEditorInput, shouldShowClearEditingSessionConfirmation } from '../widgetHosts/editor/chatEditorInput.js';
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { ChatConfiguration } from '../../common/constants.js';
Expand All @@ -33,6 +33,7 @@ import { ActiveEditorContext, AuxiliaryBarMaximizedContext } from '../../../../c
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { coalesce } from '../../../../../base/common/arrays.js';

//#region Chat View

Expand Down Expand Up @@ -381,24 +382,27 @@ abstract class BaseAgentSessionAction extends Action2 {
const agentSessionsService = accessor.get(IAgentSessionsService);
const viewsService = accessor.get(IViewsService);

let session: IAgentSession | undefined;
let sessions: IAgentSession[] = [];
if (isMarshalledAgentSessionContext(context)) {
session = agentSessionsService.getSession(context.session.resource);
} else {
session = context;
sessions = coalesce((context.sessions ?? [context.session]).map(session => agentSessionsService.getSession(session.resource)));
} else if (context) {
sessions = [context];
}

if (!session) {
if (sessions.length === 0) {
const chatView = viewsService.getActiveViewWithId<ChatViewPane>(ChatViewId);
session = chatView?.getFocusedSessions().at(0);
const focused = chatView?.getFocusedSessions().at(0);
if (focused) {
sessions = [focused];
}
}

if (session) {
await this.runWithSession(session, accessor);
if (sessions.length > 0) {
await this.runWithSessions(sessions, accessor);
}
}

abstract runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise<void> | void;
abstract runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise<void> | void;
}

export class MarkAgentSessionUnreadAction extends BaseAgentSessionAction {
Expand All @@ -419,8 +423,10 @@ export class MarkAgentSessionUnreadAction extends BaseAgentSessionAction {
});
}

runWithSession(session: IAgentSession): void {
session.setRead(false);
runWithSessions(sessions: IAgentSession[]): void {
for (const session of sessions) {
session.setRead(false);
}
}
}

Expand All @@ -442,8 +448,10 @@ export class MarkAgentSessionReadAction extends BaseAgentSessionAction {
});
}

runWithSession(session: IAgentSession): void {
session.setRead(true);
runWithSessions(sessions: IAgentSession[]): void {
for (const session of sessions) {
session.setRead(true);
}
}
}

Expand Down Expand Up @@ -477,20 +485,37 @@ export class ArchiveAgentSessionAction extends BaseAgentSessionAction {
});
}

async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise<void> {
async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise<void> {
const chatService = accessor.get(IChatService);
const chatModel = chatService.getSession(session.resource);
const dialogService = accessor.get(IDialogService);

if (chatModel && !await showClearEditingSessionConfirmation(chatModel, dialogService, {
isArchiveAction: true,
titleOverride: localize('archiveSession', "Archive chat with pending edits?"),
messageOverride: localize('archiveSessionDescription', "You have pending changes in this chat session.")
})) {
return;
// Count sessions with pending changes
let sessionsWithPendingChangesCount = 0;
for (const session of sessions) {
const chatModel = chatService.getSession(session.resource);
if (chatModel && shouldShowClearEditingSessionConfirmation(chatModel, { isArchiveAction: true })) {
sessionsWithPendingChangesCount++;
}
}

// If there are sessions with pending changes, ask for confirmation once
if (sessionsWithPendingChangesCount > 0) {
const confirmed = await dialogService.confirm({
message: sessionsWithPendingChangesCount === 1
? localize('archiveSessionWithPendingEdits', "One session has pending edits. Are you sure you want to archive?")
: localize('archiveSessionsWithPendingEdits', "{0} sessions have pending edits. Are you sure you want to archive?", sessionsWithPendingChangesCount),
primaryButton: localize('archiveSession.archive', "Archive")
});

if (!confirmed.confirmed) {
return;
}
}

session.setArchived(true);
// Archive all sessions
for (const session of sessions) {
session.setArchived(true);
}
}
}

Expand Down Expand Up @@ -526,8 +551,10 @@ export class UnarchiveAgentSessionAction extends BaseAgentSessionAction {
});
}

runWithSession(session: IAgentSession): void {
session.setArchived(false);
runWithSessions(sessions: IAgentSession[]): void {
for (const session of sessions) {
session.setArchived(false);
}
}
}

Expand All @@ -537,6 +564,7 @@ export class RenameAgentSessionAction extends BaseAgentSessionAction {
super({
id: AGENT_SESSION_RENAME_ACTION_ID,
title: localize2('rename', "Rename..."),
precondition: ChatContextKeys.hasMultipleAgentSessionsSelected.negate(),
keybinding: {
primary: KeyCode.F2,
mac: {
Expand All @@ -557,7 +585,12 @@ export class RenameAgentSessionAction extends BaseAgentSessionAction {
});
}

async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise<void> {
async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise<void> {
const session = sessions.at(0);
if (!session) {
return;
}

const quickInputService = accessor.get(IQuickInputService);
const chatService = accessor.get(IChatService);

Expand All @@ -583,13 +616,19 @@ export class DeleteAgentSessionAction extends BaseAgentSessionAction {
});
}

async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise<void> {
async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise<void> {
if (sessions.length === 0) {
return;
}

const chatService = accessor.get(IChatService);
const dialogService = accessor.get(IDialogService);
const widgetService = accessor.get(IChatWidgetService);

const confirmed = await dialogService.confirm({
message: localize('deleteSession.confirm', "Are you sure you want to delete this chat session?"),
message: sessions.length === 1
? localize('deleteSession.confirm', "Are you sure you want to delete this chat session?")
: localize('deleteSessions.confirm', "Are you sure you want to delete {0} chat sessions?", sessions.length),
detail: localize('deleteSession.detail', "This action cannot be undone."),
primaryButton: localize('deleteSession.delete', "Delete")
});
Expand All @@ -598,11 +637,14 @@ export class DeleteAgentSessionAction extends BaseAgentSessionAction {
return;
}

// Clear chat widget
await widgetService.getWidgetBySessionResource(session.resource)?.clear();
for (const session of sessions) {

// Remove from storage
await chatService.removeHistoryEntry(session.resource);
// Clear chat widget
await widgetService.getWidgetBySessionResource(session.resource)?.clear();

// Remove from storage
await chatService.removeHistoryEntry(session.resource);
}
}
}

Expand Down Expand Up @@ -651,15 +693,18 @@ export class DeleteAllLocalSessionsAction extends Action2 {

abstract class BaseOpenAgentSessionAction extends BaseAgentSessionAction {

async runWithSession(session: IAgentSession, accessor: ServicesAccessor): Promise<void> {
async runWithSessions(sessions: IAgentSession[], accessor: ServicesAccessor): Promise<void> {
const chatWidgetService = accessor.get(IChatWidgetService);

const uri = session.resource;
const targetGroup = this.getTargetGroup();
for (const session of sessions) {
const uri = session.resource;

await chatWidgetService.openSession(uri, this.getTargetGroup(), {
...this.getOptions(),
pinned: true
});
await chatWidgetService.openSession(uri, targetGroup, {
...this.getOptions(),
pinned: true
});
}
}

protected abstract getTargetGroup(): PreferredGroup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
private focusedAgentSessionArchivedContextKey: IContextKey<boolean>;
private focusedAgentSessionReadContextKey: IContextKey<boolean>;
private focusedAgentSessionTypeContextKey: IContextKey<string>;
private hasMultipleAgentSessionsSelectedContextKey: IContextKey<boolean>;

constructor(
private readonly container: HTMLElement,
Expand All @@ -89,6 +90,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
this.focusedAgentSessionArchivedContextKey = ChatContextKeys.isArchivedAgentSession.bindTo(this.contextKeyService);
this.focusedAgentSessionReadContextKey = ChatContextKeys.isReadAgentSession.bindTo(this.contextKeyService);
this.focusedAgentSessionTypeContextKey = ChatContextKeys.agentSessionType.bindTo(this.contextKeyService);
this.hasMultipleAgentSessionsSelectedContextKey = ChatContextKeys.hasMultipleAgentSessionsSelected.bindTo(this.contextKeyService);

this.createList(this.container);

Expand Down Expand Up @@ -143,7 +145,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
dnd: this.instantiationService.createInstance(AgentSessionsDragAndDrop),
identityProvider: new AgentSessionsIdentityProvider(),
horizontalScrolling: false,
multipleSelectionSupport: false,
multipleSelectionSupport: true,
findWidgetEnabled: true,
defaultFindMode: TreeFindMode.Filter,
keyboardNavigationLabelProvider: new AgentSessionsKeyboardNavigationLabelProvider(),
Expand Down Expand Up @@ -183,7 +185,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
}
}));

this._register(Event.any(list.onDidChangeFocus, model.onDidChangeSessions)(() => {
this._register(Event.any(list.onDidChangeFocus, list.onDidChangeSelection, model.onDidChangeSessions)(() => {
const focused = list.getFocus().at(0);
if (focused && isAgentSession(focused)) {
this.focusedAgentSessionArchivedContextKey.set(focused.isArchived());
Expand All @@ -194,6 +196,9 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
this.focusedAgentSessionReadContextKey.reset();
this.focusedAgentSessionTypeContextKey.reset();
}

const selection = list.getSelection().filter(isAgentSession);
this.hasMultipleAgentSessionsSelectedContextKey.set(selection.length > 1);
}));
}

Expand Down Expand Up @@ -250,11 +255,17 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo

const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay));

const marshalledSession: IMarshalledAgentSessionContext = { session, $mid: MarshalledId.AgentSessionContext };
const selection = this.sessionsList?.getSelection().filter(isAgentSession) ?? [];
const marshalledContext: IMarshalledAgentSessionContext = {
session,
sessions: selection.length > 1 && selection.includes(session) ? selection : [session],
$mid: MarshalledId.AgentSessionContext
};

this.contextMenuService.showContextMenu({
getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)),
getActions: () => Separator.join(...menu.getActions({ arg: marshalledContext, shouldForwardArgs: true }).map(([, actions]) => actions)),
getAnchor: () => anchor,
getActionsContext: () => marshalledSession,
getActionsContext: () => marshalledContext,
});

menu.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ export function isAgentSessionSection(obj: unknown): obj is IAgentSessionSection

export interface IMarshalledAgentSessionContext {
readonly $mid: MarshalledId.AgentSessionContext;

readonly session: IAgentSession;
readonly sessions: IAgentSession[]; // support for multi-selection
}

export function isMarshalledAgentSessionContext(thing: unknown): thing is IMarshalledAgentSessionContext {
Expand Down Expand Up @@ -370,7 +372,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
providerType: chatSessionType,
providerLabel,
resource: session.resource,
label: session.label,
label: session.label.split('\n')[0], // protect against weird multi-line labels that break our layout
description: session.description,
icon,
badge: session.badge,
Expand Down
Loading
Loading