From bd4f84cc87f1f94ea1ff18d668c14d43d43df8ea Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Sun, 25 Jan 2026 13:26:48 +0100 Subject: [PATCH] Reveal session when following up (#289694) --- .../chatEditingCodeEditorIntegration.ts | 12 ++++-- .../chatEditing/chatEditingEditorActions.ts | 39 +++++++++++++++++-- .../chatEditingExplanationWidget.ts | 23 +++++++---- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index 1c354f5d1e3..4deffa74db3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -12,6 +12,7 @@ import { DisposableStore, dispose, IDisposable, toDisposable } from '../../../.. import { autorun, constObservable, derived, IObservable, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../editor/browser/observableCodeEditor.js'; import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from '../../../../../editor/browser/widget/diffEditor/components/accessibleDiffViewer.js'; @@ -124,7 +125,10 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito this._explanationWidgetManager = this._store.add(new ChatEditingExplanationWidgetManager(this._editor, this._chatWidgetService, this._viewsService)); // Pass the current diff info when creating lazily (untracked - diff changes handled separately) const diff = documentDiffInfo.read(undefined); - this._explanationWidgetManager.update(diff, true); + // Find the session that owns this entry to get the chat session resource + const session = this._chatEditingService.editingSessionsObs.read(undefined) + .find(candidate => candidate.getEntry(this._entry.modifiedURI)); + this._explanationWidgetManager.update(diff, true, session?.chatSessionResource); // Generate explanations asynchronously ChatEditingExplanationWidgetManager.generateExplanations( this._explanationWidgetManager.widgets, @@ -500,15 +504,17 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito } // Update explanation widgets for chat changes - // Get visibility from session that owns this entry + // Get visibility and session resource from session that owns this entry let explanationVisible = false; + let chatSessionResource: URI | undefined; for (const session of this._chatEditingService.editingSessionsObs.get()) { if (session.entries.get().some((e: IModifiedFileEntry) => e.entryId === this._entry.entryId)) { explanationVisible = session.explanationWidgetVisible.get(); + chatSessionResource = session.chatSessionResource; break; } } - this._explanationWidgetManager?.update(diff, explanationVisible); + this._explanationWidgetManager?.update(diff, explanationVisible, chatSessionResource); const positionObs = observableFromEvent(this._editor.onDidChangeCursorPosition, _ => this._editor.getPosition()); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index dcc111a1878..2c23f52393c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -23,7 +23,7 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; import { CTX_HOVER_MODE } from '../../../inlineChat/common/inlineChat.js'; import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; -import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; +import { IDocumentDiffItemWithMultiDiffEditorItem, MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_FOCUSED } from '../../../notebook/common/notebookContextKeys.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, IChatEditingService, IChatEditingSession, IModifiedFileEntry, IModifiedFileEntryChangeHunk, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState, parseChatMultiDiffUri } from '../../common/editing/chatEditingService.js'; @@ -35,6 +35,7 @@ import { DiffEditorViewModel } from '../../../../../editor/browser/widget/diffEd import { IChatWidgetService } from '../chat.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { ChatConfiguration } from '../../common/constants.js'; @@ -470,6 +471,8 @@ abstract class MultiDiffAcceptDiscardAction extends Action2 { } +const explainMultiDiffSchemes = [CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, 'copilotcli-worktree-changes', 'copilotcloud-pr-changes']; + class ExplainMultiDiffAction extends Action2 { private readonly _widgetsByInput = new WeakMap(); @@ -479,7 +482,7 @@ class ExplainMultiDiffAction extends Action2 { id: 'chatEditing.multidiff.explain', title: localize('explain', 'Explain'), menu: { - when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('resourceScheme', 'copilotcli-worktree-changes'), ContextKeyExpr.equals('resourceScheme', 'copilotcloud-pr-changes')), ContextKeyExpr.has(`config.${ChatConfiguration.ExplainChangesEnabled}`)), + when: ContextKeyExpr.and(ContextKeyExpr.or(...explainMultiDiffSchemes.map(scheme => ContextKeyExpr.equals('resourceScheme', scheme))), ContextKeyExpr.has(`config.${ChatConfiguration.ExplainChangesEnabled}`)), id: MenuId.MultiDiffEditorContent, order: 10, }, @@ -491,6 +494,7 @@ class ExplainMultiDiffAction extends Action2 { const languageModelsService = accessor.get(ILanguageModelsService); const chatWidgetService = accessor.get(IChatWidgetService); const viewsService = accessor.get(IViewsService); + const chatEditingService = accessor.get(IChatEditingService); const activePane = editorService.activeEditorPane; if (!activePane) { @@ -521,6 +525,35 @@ class ExplainMultiDiffAction extends Action2 { const viewModel = activePane.viewModel; const items = viewModel.items.get(); + // Try to extract chat session resource from the multi-diff editor URI or by scanning sessions + let chatSessionResource: URI | undefined; + if (input instanceof MultiDiffEditorInput && input.resource?.scheme === CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME) { + chatSessionResource = parseChatMultiDiffUri(input.resource).chatSessionResource; + } + if (!chatSessionResource) { + // Scan sessions to find one that owns files in this multi-diff editor + // Use goToFileUri if available, otherwise extract file path from the modified URI + const fileUris = items.map(item => { + const docDiffItem = item.documentDiffItem as IDocumentDiffItemWithMultiDiffEditorItem | undefined; + const goToFileUri = docDiffItem?.multiDiffEditorItem?.goToFileUri; + if (goToFileUri) { + return goToFileUri; + } + // Fallback: extract file path from the modified URI (e.g., git: URIs have the path) + const modifiedUri = docDiffItem?.multiDiffEditorItem?.modifiedUri ?? item.modifiedUri; + if (modifiedUri?.path) { + return URI.file(modifiedUri.path); + } + return undefined; + }).filter((uri): uri is URI => !!uri); + for (const session of chatEditingService.editingSessionsObs.get()) { + if (fileUris.some(uri => session.getEntry(uri))) { + chatSessionResource = session.chatSessionResource; + break; + } + } + } + // First pass: collect all diffs grouped by file const diffsByFile = new Map { e.stopPropagation(); - const chatWidget = this._chatWidgetService.lastFocusedWidget; + const range = new Range(exp.startLineNumber, 1, exp.endLineNumber, 1); + let chatWidget: IChatWidget | undefined; + if (this._chatSessionResource) { + chatWidget = await this._chatWidgetService.openSession(this._chatSessionResource, ChatViewPaneTarget); + } else { + await this._viewsService.openView(ChatViewId, true); + chatWidget = this._chatWidgetService.lastFocusedWidget; + } if (chatWidget) { - const range = new Range(exp.startLineNumber, 1, exp.endLineNumber, 1); chatWidget.attachmentModel.addContext( chatWidget.attachmentModel.asFileVariableEntry(this._uri, range) ); } - await this._viewsService.openView(ChatViewId, true); })); // Click on item to mark as read @@ -592,9 +599,10 @@ export class ChatEditingExplanationWidgetManager extends Disposable { /** * Updates the diff info and generates explanations. * Creates widgets immediately and starts LLM generation. - * @param visible Whether widgets should be visible (default: false) + * @param visible Whether widgets should be visible + * @param chatSessionResource Chat session resource to open when following up, or undefined */ - update(diffInfo: IExplanationDiffInfo, visible: boolean = false): void { + update(diffInfo: IExplanationDiffInfo, visible: boolean, chatSessionResource: URI | undefined): void { this._modelUri = diffInfo.modifiedModel.uri; // Clear existing widgets @@ -616,6 +624,7 @@ export class ChatEditingExplanationWidgetManager extends Disposable { diffInfo, this._chatWidgetService, this._viewsService, + chatSessionResource, ); this._widgets.push(widget); this._register(widget); @@ -650,7 +659,7 @@ export class ChatEditingExplanationWidgetManager extends Disposable { widgets: readonly ChatEditingExplanationWidget[], diffInfo: IExplanationDiffInfo, languageModelsService: ILanguageModelsService, - cancellationToken: import('../../../../../base/common/cancellation.js').CancellationToken + cancellationToken: CancellationToken ): Promise { if (diffInfo.changes.length === 0) { return;