Skip to content
Closed
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
123 changes: 117 additions & 6 deletions src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb
import { IChatService } from '../../common/chatService.js';
import { IChatSessionItem, IChatSessionsService } from '../../common/chatSessionsService.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import Severity from '../../../../../base/common/severity.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
import { ChatEditorInput } from '../chatEditorInput.js';
import { ChatEditor } from '../chatEditor.js';
import { CHAT_CATEGORY } from './chatActions.js';
import { AUX_WINDOW_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js';
import { IChatEditorOptions } from '../chatEditor.js';
import { ChatSessionUri } from '../../common/chatUri.js';
import { ILocalChatSessionItem, VIEWLET_ID } from '../chatSessions.js';
import { GroupDirection, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
import { ChatViewId } from '../chat.js';
import { ChatViewId, IChatWidgetService } from '../chat.js';
import { ChatViewPane } from '../chatViewPane.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { ChatConfiguration } from '../../common/constants.js';
Expand Down Expand Up @@ -54,6 +56,98 @@ function isLocalChatSessionItem(item: IChatSessionItem): item is ILocalChatSessi
return ('editor' in item && 'group' in item) || ('widget' in item && 'sessionType' in item);
}

/**
* Helper method to find and close all existing instances of a chat session.
* This ensures we implement "move" behavior instead of "duplicate" behavior.
* Returns view state if available for preserving user's position.
*/
async function findAndCloseExistingSessionInstances(
accessor: ServicesAccessor,
sessionId: string
): Promise<any> {
let preservedViewState: any = undefined;

try {
// First, try to get view state from widget before clearing it
const widgetService = accessor.get(IChatWidgetService);
const widget = widgetService.getWidgetBySessionId(sessionId);
if (widget) {
preservedViewState = widget.getViewState?.();
}

// Close widget instances
await clearWidgetBySessionId(accessor, sessionId);

// Close editor instances (may also preserve view state)
const editorViewState = await closeEditorsBySessionId(accessor, sessionId);
if (!preservedViewState && editorViewState) {
preservedViewState = editorViewState;
}

return preservedViewState;
} catch (error) {
// Log error but don't fail the operation
const logService = accessor.get(ILogService);
logService.warn('Failed to close existing session instances', error);
return preservedViewState;
}
}

/**
* Helper method to close all editor instances matching a session ID
* Returns view state from the closed editor if available
*/
async function closeEditorsBySessionId(
accessor: ServicesAccessor,
sessionId: string
): Promise<any> {
const editorService = accessor.get(IEditorService);
const editorGroupService = accessor.get(IEditorGroupsService);

let preservedViewState: any = undefined;
const editorsToClose: Array<{ editor: ChatEditorInput; groupId: number }> = [];

// Find all chat editors matching the session ID
for (const group of editorGroupService.groups) {
for (const editor of group.editors) {
if (editor instanceof ChatEditorInput && editor.sessionId === sessionId) {
// Try to get view state from active editor if this is the active one
if (group.activeEditor === editor) {
const activePane = editorService.activeEditorPane;
if (activePane instanceof ChatEditor) {
preservedViewState = activePane.getViewState?.();
}
}
editorsToClose.push({ editor, groupId: group.id });
}
}
}

// Close all matching editors
for (const { editor, groupId } of editorsToClose) {
await editorService.closeEditor({ editor, groupId });
}

return preservedViewState;
}

/**
* Helper method to clear all widget instances matching a session ID
*/
async function clearWidgetBySessionId(
accessor: ServicesAccessor,
sessionId: string
): Promise<void> {
const widgetService = accessor.get(IChatWidgetService);

// Find and clear the widget with the matching session ID
const widget = widgetService.getWidgetBySessionId(sessionId);
if (widget) {
widget.clear();
await widget.waitForReady();
}
}

function isMarshalledChatSessionContext(obj: unknown): obj is IMarshalledChatSessionContext {
return !!obj &&
typeof obj === 'object' &&
Expand Down Expand Up @@ -288,21 +382,27 @@ export class OpenChatSessionInNewWindowAction extends Action2 {
sessionId = context.sessionId;
}

// Close existing instances before opening in new window (implements move behavior)
if (sessionItem && (isLocalChatSessionItem(sessionItem) || sessionId.startsWith('history-'))) {
// For history session remove the `history` prefix
const sessionIdWithoutHistory = sessionId.replace('history-', '');
const preservedViewState = await findAndCloseExistingSessionInstances(accessor, sessionIdWithoutHistory);

const options: IChatEditorOptions = {
target: { sessionId: sessionIdWithoutHistory },
pinned: true,
auxiliary: { compact: false },
ignoreInView: true
ignoreInView: true,
viewState: preservedViewState
};
// For local sessions, create a new chat editor in the auxiliary window
await editorService.openEditor({
resource: ChatEditorInput.getNewEditorUri(),
options,
}, AUX_WINDOW_GROUP);
} else {
// For external provider sessions, close existing instances first
await findAndCloseExistingSessionInstances(accessor, sessionId);

// For external provider sessions, open the existing session in the auxiliary window
const providerType = sessionItem && (sessionItem as any).provider?.chatSessionType || 'external';
await editorService.openEditor({
Expand Down Expand Up @@ -360,21 +460,28 @@ export class OpenChatSessionInNewEditorGroupAction extends Action2 {
sessionId = context.sessionId;
}

// Close existing instances before opening in new editor group (implements move behavior)
// Create a new editor group to the right
const newGroup = editorGroupService.addGroup(editorGroupService.activeGroup, GroupDirection.RIGHT);
if (sessionItem && (isLocalChatSessionItem(sessionItem) || sessionId.startsWith('history-'))) {
const sessionIdWithoutHistory = sessionId.replace('history-', '');
const preservedViewState = await findAndCloseExistingSessionInstances(accessor, sessionIdWithoutHistory);

const options: IChatEditorOptions = {
target: { sessionId: sessionIdWithoutHistory },
pinned: true,
ignoreInView: true,
viewState: preservedViewState
};
// For local sessions, create a new chat editor
await editorService.openEditor({
resource: ChatEditorInput.getNewEditorUri(),
options,
}, newGroup.id);
} else {
// For external provider sessions, close existing instances first
await findAndCloseExistingSessionInstances(accessor, sessionId);

// For external provider sessions, open the existing session
const providerType = sessionItem && (sessionItem as any).provider?.chatSessionType || 'external';
await editorService.openEditor({
Expand Down Expand Up @@ -428,20 +535,24 @@ export class OpenChatSessionInSidebarAction extends Action2 {
sessionId = context.sessionId;
}

// Close existing instances before opening in sidebar (implements move behavior)
// Open the chat view in the sidebar
const chatViewPane = await viewsService.openView(ChatViewId) as ChatViewPane;
if (chatViewPane) {
// Handle different session types
if (sessionItem && (isLocalChatSessionItem(sessionItem) || sessionId.startsWith('history-'))) {
// For local sessions and history sessions, remove the 'history-' prefix if present
const sessionIdWithoutHistory = sessionId.replace('history-', '');
// Load using the session ID directly
await chatViewPane.loadSession(sessionIdWithoutHistory);
const preservedViewState = await findAndCloseExistingSessionInstances(accessor, sessionIdWithoutHistory);
// Load using the session ID directly with preserved view state
await chatViewPane.loadSession(sessionIdWithoutHistory, preservedViewState);
} else {
// For external provider sessions, close existing instances first
const preservedViewState = await findAndCloseExistingSessionInstances(accessor, sessionId);
// For external provider sessions, create a URI and load using that
const providerType = sessionItem && (sessionItem as any).provider?.chatSessionType || 'external';
const sessionUri = ChatSessionUri.forSession(providerType, sessionId);
await chatViewPane.loadSession(sessionUri);
await chatViewPane.loadSession(sessionUri, preservedViewState);
}

// Focus the chat input
Expand Down
Loading