Reveal session when following up (#289694)

This commit is contained in:
Christof Marti
2026-01-25 13:26:48 +01:00
parent 4de5008c32
commit bd4f84cc87
3 changed files with 61 additions and 13 deletions

View File

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

View File

@@ -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<EditorInput, DisposableStore>();
@@ -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<string, {
editor: ICodeEditor;
@@ -587,7 +620,7 @@ class ExplainMultiDiffAction extends Action2 {
widgetsStore.add(manager);
// Update with diff info and show (but don't generate explanations yet)
manager.update(diffInfo, true);
manager.update(diffInfo, true, chatSessionResource);
managersWithDiffInfo.push({ manager, diffInfo });
}

View File

@@ -22,10 +22,11 @@ import { overviewRulerRangeHighlight } from '../../../../../editor/common/core/e
import { IEditorDecorationsCollection } from '../../../../../editor/common/editorCommon.js';
import { ITextModel, OverviewRulerLane } from '../../../../../editor/common/model.js';
import { themeColorFromId } from '../../../../../platform/theme/common/themeService.js';
import { ChatViewId, IChatWidgetService } from '../chat.js';
import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService } from '../chat.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
import * as nls from '../../../../../nls.js';
import { basename } from '../../../../../base/common/resources.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
/**
* Simple diff info interface for explanation widgets
@@ -147,6 +148,7 @@ export class ChatEditingExplanationWidget extends Disposable implements IOverlay
diffInfo: IExplanationDiffInfo,
private readonly _chatWidgetService: IChatWidgetService,
private readonly _viewsService: IViewsService,
private readonly _chatSessionResource?: URI,
) {
super();
@@ -361,14 +363,19 @@ export class ChatEditingExplanationWidget extends Disposable implements IOverlay
// Reply button click handler
this._eventStore.add(addDisposableListener(replyButton, 'click', async (e) => {
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<void> {
if (diffInfo.changes.length === 0) {
return;