From 1eea41f3b8dcd80f2f2b49e1a8e3cd859872c8f5 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:55:35 -0800 Subject: [PATCH] Apply and file changes part for worktree (#281410) * api idea * iterate * start hacking * wip * use the right menu * update * update * Fixes * Fix test and override --------- Co-authored-by: Connor Peet --- src/vs/base/common/iterator.ts | 4 +- src/vs/platform/actions/common/actions.ts | 1 + .../api/browser/mainThreadChatSessions.ts | 29 +-- .../workbench/api/common/extHost.api.impl.ts | 1 + .../api/common/extHostChatSessions.ts | 12 +- src/vs/workbench/api/common/extHostTypes.ts | 4 + .../agentSessions/agentSessionsActions.ts | 4 +- .../agentSessions/agentSessionsModel.ts | 35 +++- .../agentSessions/agentSessionsViewer.ts | 10 +- .../localAgentSessionsProvider.ts | 4 +- .../chatReferencesContentPart.ts | 97 +++++++-- .../browser/chatEditing/chatEditingActions.ts | 57 +++++- .../contrib/chat/browser/chatInputPart.ts | 188 +++++++++++++----- .../chatSessions/view/sessionsTreeRenderer.ts | 17 +- .../contrib/chat/browser/media/chat.css | 35 ++++ .../contrib/chat/common/chatContextKeys.ts | 1 + .../contrib/chat/common/chatService.ts | 1 + .../chat/common/chatSessionsService.ts | 11 +- .../browser/agentSessionViewModel.test.ts | 4 +- .../localAgentSessionsProvider.test.ts | 11 +- .../test/browser/inlineChatController.test.ts | 13 ++ .../actions/common/menusExtensionPoint.ts | 6 + .../vscode.proposed.chatSessionsProvider.d.ts | 26 ++- 23 files changed, 458 insertions(+), 113 deletions(-) diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 54db9a8c5b7..1e8c84d9af9 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -12,8 +12,8 @@ export namespace Iterable { } const _empty: Iterable = Object.freeze([]); - export function empty(): Iterable { - return _empty as Iterable; + export function empty(): readonly never[] { + return _empty as readonly never[]; } export function* single(element: T): Iterable { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 268eb3083cf..9de72fa0378 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -255,6 +255,7 @@ export class MenuId { static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly ChatModePicker = new MenuId('ChatModePicker'); static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar'); + static readonly ChatEditingSessionChangesToolbar = new MenuId('ChatEditingSessionChangesToolbar'); static readonly ChatEditingEditorContent = new MenuId('ChatEditingEditorContent'); static readonly ChatEditingEditorHunk = new MenuId('ChatEditingEditorHunk'); static readonly ChatEditingDeletedNotebookCell = new MenuId('ChatEditingDeletedNotebookCell'); diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 502294b7652..c6b0d3938b1 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -459,29 +459,33 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat const uri = URI.revive(session.resource); const model = this._chatService.getSession(uri); let description: string | undefined; - let statistics: IChatSessionItem['statistics']; + let changes: IChatSessionItem['changes']; if (model) { description = this._chatSessionsService.getSessionDescription(model); } - const modelStats = model ? - await awaitStatsForSession(model) : - (await this._chatService.getMetadataForSession(uri))?.stats; - if (modelStats) { - statistics = { - files: modelStats.fileCount, - insertions: modelStats.added, - deletions: modelStats.removed - }; + if (session.changes instanceof Array) { + changes = revive(session.changes); + } else { + const modelStats = model ? + await awaitStatsForSession(model) : + (await this._chatService.getMetadataForSession(uri))?.stats; + if (modelStats) { + changes = { + files: modelStats.fileCount, + insertions: modelStats.added, + deletions: modelStats.removed + }; + } } return { ...session, + changes, resource: uri, iconPath: session.iconPath, tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined, - description: description || session.description, - statistics + description: description || session.description } satisfies IChatSessionItem; })); } catch (error) { @@ -498,6 +502,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat } return { ...chatSessionItem, + changes: revive(chatSessionItem.changes), resource: URI.revive(chatSessionItem.resource), iconPath: chatSessionItem.iconPath, tooltip: chatSessionItem.tooltip ? this._reviveTooltip(chatSessionItem.tooltip) : undefined, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ddb081274e5..105a583456a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1864,6 +1864,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I EditSessionIdentityMatch: EditSessionIdentityMatch, InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, ChatCopyKind: extHostTypes.ChatCopyKind, + ChatSessionChangedFile: extHostTypes.ChatSessionChangedFile, ChatEditingSessionActionOutcome: extHostTypes.ChatEditingSessionActionOutcome, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, DebugStackFrame: extHostTypes.DebugStackFrame, diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 7b166295b61..9fa8950e255 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -186,11 +186,13 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio startTime: sessionContent.timing?.startTime ?? 0, endTime: sessionContent.timing?.endTime }, - statistics: sessionContent.statistics ? { - files: sessionContent.statistics?.files ?? 0, - insertions: sessionContent.statistics?.insertions ?? 0, - deletions: sessionContent.statistics?.deletions ?? 0 - } : undefined + changes: sessionContent.changes instanceof Array + ? sessionContent.changes : + (sessionContent.changes && { + files: sessionContent.changes?.files ?? 0, + insertions: sessionContent.changes?.insertions ?? 0, + deletions: sessionContent.changes?.deletions ?? 0, + }), }; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index f41e201c1cf..5d7fc7c00a9 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3425,6 +3425,10 @@ export enum ChatSessionStatus { InProgress = 2 } +export class ChatSessionChangedFile { + constructor(public readonly modifiedUri: vscode.Uri, public readonly insertions: number, public readonly deletions: number, public readonly originalUri?: vscode.Uri) { } +} + export enum ChatResponseReferencePartStatusKind { Complete = 1, Partial = 2, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 20ff362466f..6eae772e23c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -5,7 +5,7 @@ import './media/agentsessionsactions.css'; import { localize, localize2 } from '../../../../../nls.js'; -import { IAgentSession } from './agentSessionsModel.js'; +import { getAgentChangesSummary, IAgentSession } from './agentSessionsModel.js'; import { Action, IAction } from '../../../../../base/common/actions.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -112,7 +112,7 @@ export class AgentSessionDiffActionViewItem extends ActionViewItem { label.textContent = ''; const session = this.action.getSession(); - const diff = session.statistics; + const diff = getAgentChangesSummary(session.changes); if (!diff) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 744b37f3e5c..6b4020c45f3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; -import { ChatSessionStatus, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; +import { ChatSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; //#region Interfaces, Types @@ -56,13 +56,32 @@ interface IAgentSessionData { readonly finishedOrFailedTime?: number; }; - readonly statistics?: { + readonly changes?: readonly IChatSessionFileChange[] | { readonly files: number; readonly insertions: number; readonly deletions: number; }; } +export function getAgentChangesSummary(changes: IAgentSession['changes']) { + if (!changes) { + return; + } + + if (!(changes instanceof Array)) { + return changes; + } + + let insertions = 0; + let deletions = 0; + for (const change of changes) { + insertions += change.insertions; + deletions += change.deletions; + } + + return { files: changes.length, insertions, deletions }; +} + export interface IAgentSession extends IAgentSessionData { isArchived(): boolean; setArchived(archived: boolean): void; @@ -264,6 +283,11 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode }); } + const changes = session.changes; + const normalizedChanges = changes && !(changes instanceof Array) + ? { files: changes.files, insertions: changes.insertions, deletions: changes.deletions } + : changes; + sessions.set(session.resource, this.toAgentSession({ providerType: provider.chatSessionType, providerLabel, @@ -280,7 +304,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode inProgressTime, finishedOrFailedTime }, - statistics: session.statistics, + changes: normalizedChanges, })); } } @@ -365,6 +389,7 @@ interface ISerializedAgentSession { readonly files: number; readonly insertions: number; readonly deletions: number; + readonly details: readonly IChatSessionFileChange[]; }; } @@ -413,7 +438,7 @@ class AgentSessionsCache { endTime: session.timing.endTime, }, - statistics: session.statistics, + changes: session.changes, })); this.storageService.store(AgentSessionsCache.SESSIONS_STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -446,7 +471,7 @@ class AgentSessionsCache { endTime: session.timing.endTime, }, - statistics: session.statistics, + changes: session.statistics, })); } catch { return []; // invalid data in storage, fallback to empty sessions list diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index facb08beca9..895859cafca 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -157,10 +157,12 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer 0 || diff.insertions > 0 || diff.deletions > 0)) { - const diffAction = template.elementDisposable.add(new AgentSessionShowDiffAction(session.element)); - template.detailsToolbar.push([diffAction], { icon: false, label: true }); + const { changes: diff } = session.element; + if (session.element.status !== ChatSessionStatus.InProgress && diff) { + if (diff instanceof Array ? diff.length > 0 : (diff.files > 0 || diff.insertions > 0 || diff.deletions > 0)) { + const diffAction = template.elementDisposable.add(new AgentSessionShowDiffAction(session.element)); + template.detailsToolbar.push([diffAction], { icon: false, label: true }); + } } // Description otherwise diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts index 022f21ce99a..719b4e0f3c0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsProvider.ts @@ -151,10 +151,10 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess startTime, endTime }, - statistics: chat.stats ? { + changes: chat.stats ? { insertions: chat.stats.added, deletions: chat.stats.removed, - files: chat.stats.fileCount + files: chat.stats.fileCount, } : undefined }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index da18a87674a..a3ceae76572 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -56,7 +56,15 @@ export interface IChatReferenceListItem extends IChatContentReference { excluded?: boolean; } -export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; +export interface IChatListDividerItem { + kind: 'divider'; + label: string; + menuId?: MenuId; + menuArg?: unknown; + scopedInstantiationService?: IInstantiationService; +} + +export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage | IChatListDividerItem; export class ChatCollapsibleListContentPart extends ChatCollapsibleContentPart { @@ -205,7 +213,7 @@ export class CollapsibleListPool extends Disposable { 'ChatListRenderer', container, new CollapsibleListDelegate(), - [this.instantiationService.createInstance(CollapsibleListRenderer, resourceLabels, this.menuId)], + [this.instantiationService.createInstance(CollapsibleListRenderer, resourceLabels, this.menuId), this.instantiationService.createInstance(DividerRenderer)], { ...this.listOptions, alwaysConsumeMouseWheel: false, @@ -214,6 +222,9 @@ export class CollapsibleListPool extends Disposable { if (element.kind === 'warning') { return element.content.value; } + if (element.kind === 'divider') { + return element.label; + } const reference = element.reference; if (typeof reference === 'string') { return reference; @@ -278,6 +289,9 @@ class CollapsibleListDelegate implements IListVirtualDelegate { + static TEMPLATE_ID = 'chatListDividerRenderer'; + readonly templateId: string = DividerRenderer.TEMPLATE_ID; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + renderTemplate(container: HTMLElement): IDividerTemplate { + const templateDisposables = new DisposableStore(); + const elementDisposables = templateDisposables.add(new DisposableStore()); + container.classList.add('chat-list-divider'); + const label = dom.append(container, dom.$('span.chat-list-divider-label')); + const line = dom.append(container, dom.$('div.chat-list-divider-line')); + const toolbarContainer = dom.append(container, dom.$('.chat-list-divider-toolbar')); + + return { container, label, line, toolbarContainer, templateDisposables, elementDisposables, toolbar: undefined }; + } + + renderElement(data: IChatListDividerItem, index: number, templateData: IDividerTemplate): void { + templateData.label.textContent = data.label; + + // Clear element-specific disposables from previous render + templateData.elementDisposables.clear(); + templateData.toolbar = undefined; + dom.clearNode(templateData.toolbarContainer); + + if (data.menuId) { + const instantiationService = data.scopedInstantiationService || this.instantiationService; + templateData.toolbar = templateData.elementDisposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, templateData.toolbarContainer, data.menuId, { menuOptions: { arg: data.menuArg } })); + } + } + + disposeTemplate(templateData: IDividerTemplate): void { + templateData.templateDisposables.dispose(); + } +} + function getResourceLabelForGithubUri(uri: URI): IResourceLabelProps { const repoPath = uri.path.split('/').slice(1, 3).join('/'); const filePath = uri.path.split('/').slice(5); @@ -492,7 +553,7 @@ function getLineRangeFromGithubUri(uri: URI): IRange | undefined { } function getResourceForElement(element: IChatCollapsibleListItem): URI | null { - if (element.kind === 'warning') { + if (element.kind === 'warning' || element.kind === 'divider') { return null; } const { reference } = element; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 499b4cec684..e472f31387d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -6,7 +6,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { basename } from '../../../../../base/common/resources.js'; +import { basename, isEqual } from '../../../../../base/common/resources.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; @@ -18,7 +18,7 @@ import { ILanguageFeaturesService } from '../../../../../editor/common/services/ import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize, localize2 } from '../../../../../nls.js'; import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; @@ -26,6 +26,7 @@ import { EditorActivation } from '../../../../../platform/editor/common/editor.j import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IEditorPane } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; import { isChatViewTitleActionContext } from '../../common/chatActions.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js'; @@ -304,6 +305,58 @@ export class ChatEditingShowChangesAction extends EditingSessionAction { } registerAction2(ChatEditingShowChangesAction); +export class ViewAllSessionChangesAction extends Action2 { + static readonly ID = 'chatEditing.viewAllSessionChanges'; + + constructor() { + super({ + id: ViewAllSessionChangesAction.ID, + title: localize2('chatEditing.viewAllSessionChanges', 'View All Changes'), + icon: Codicon.diffMultiple, + category: CHAT_CATEGORY, + precondition: ChatContextKeys.hasAgentSessionChanges, + menu: [ + { + id: MenuId.ChatEditingSessionChangesToolbar, + group: 'navigation', + order: 10, + when: ChatContextKeys.hasAgentSessionChanges + } + ], + }); + } + + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + const agentSessionsService = accessor.get(IAgentSessionsService); + const commandService = accessor.get(ICommandService); + + const chatWidget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat).find(w => w.supportsChangingModes); + if (!chatWidget?.viewModel) { + return; + } + + const sessionResource = chatWidget.viewModel.model.sessionResource; + const session = agentSessionsService.model.sessions.find(s => isEqual(s.resource, sessionResource)); + const changes = session?.changes; + if (!(changes instanceof Array)) { + return; + } + + const resources = changes + .filter(d => d.originalUri) + .map(d => ({ originalUri: d.originalUri!, modifiedUri: d.modifiedUri })); + + if (resources.length > 0) { + await commandService.executeCommand('_workbench.openMultiDiffEditor', { + title: localize('chatEditing.allChanges.title', 'All Session Changes'), + resources, + }); + } + } +} +registerAction2(ViewAllSessionChangesAction); + async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 7691c096368..47e1fa0544b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -26,7 +26,7 @@ import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposab import { ResourceSet } from '../../../../base/common/map.js'; import { MarshalledId } from '../../../../base/common/marshallingIds.js'; import { Schemas } from '../../../../base/common/network.js'; -import { autorun, derived, derivedOpts, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { autorun, derived, derivedOpts, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ScrollbarVisibility } from '../../../../base/common/scrollable.js'; @@ -63,6 +63,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { ILabelService } from '../../../../platform/label/common/label.js'; import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; @@ -81,7 +82,7 @@ import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../common/chatModel.js'; import { ChatMode, IChatMode, IChatModeService } from '../common/chatModes.js'; import { IChatFollowup, IChatService } from '../common/chatService.js'; -import { IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../common/chatSessionsService.js'; +import { IChatSessionFileChange, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../common/chatSessionsService.js'; import { getChatSessionType } from '../common/chatUri.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../common/chatVariableEntries.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; @@ -91,6 +92,7 @@ import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, IL import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { ActionLocation, ChatContinueInSessionActionItem, ContinueChatInSessionAction } from './actions/chatContinueInAction.js'; import { ChatOpenModelPickerActionId, ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenModePickerAction } from './actions/chatExecuteActions.js'; +import { IAgentSessionsService } from './agentSessions/agentSessionsService.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; @@ -98,11 +100,11 @@ import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmen import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatTodoListWidget } from './chatContentParts/chatTodoListWidget.js'; -import { ChatInputPartWidgetController } from './chatInputPartWidgets.js'; import { IChatContextService } from './chatContextService.js'; import { ChatDragAndDrop } from './chatDragAndDrop.js'; -import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from './chatEditing/chatEditingActions.js'; +import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from './chatEditing/chatEditingActions.js'; import { ChatFollowups } from './chatFollowups.js'; +import { ChatInputPartWidgetController } from './chatInputPartWidgets.js'; import { ChatSelectedTools } from './chatSelectedTools.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from './chatSessions/chatSessionPickerActionItem.js'; import { ChatImplicitContext } from './contrib/chatImplicitContext.js'; @@ -146,7 +148,7 @@ export interface IWorkingSetEntry { export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { private static _counter = 0; - private _workingSetCollapsed = true; + private _workingSetCollapsed = observableValue('chatInputPart.workingSetCollapsed', true); private readonly _chatInputTodoListWidget = this._register(new MutableDisposable()); private readonly _chatEditingTodosDisposables = this._register(new DisposableStore()); private _lastEditingSessionResource: URI | undefined; @@ -429,6 +431,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IChatService private readonly chatService: IChatService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @IChatContextService private readonly chatContextService: IChatContextService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, ) { super(); @@ -1976,7 +1979,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (chatEditingSession) { if (!isEqual(chatEditingSession.chatSessionResource, this._lastEditingSessionResource)) { - this._workingSetCollapsed = true; + this._workingSetCollapsed.set(true, undefined); } this._lastEditingSessionResource = chatEditingSession.chatSessionResource; } @@ -1985,7 +1988,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return chatEditingSession?.entries.read(r).filter(entry => entry.state.read(r) === ModifiedFileEntryState.Modified) || []; }); - const listEntries = derived((reader): IChatCollapsibleListItem[] => { + const editSessionEntries = derived((reader): IChatCollapsibleListItem[] => { const seenEntries = new ResourceSet(); const entries: IChatCollapsibleListItem[] = []; for (const entry of modifiedEntries.read(reader)) { @@ -2022,15 +2025,43 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return entries; }); - const shouldRender = listEntries.map(r => r.length > 0); + const sessionFileChanges = observableFromEvent( + this, + this.agentSessionsService.model.onDidChangeSessions, + () => { + const sessionResource = this._widget?.viewModel?.model?.sessionResource; + if (!sessionResource) { + return Iterable.empty(); + } + const model = this.agentSessionsService.model.sessions.find(s => isEqual(s.resource, sessionResource)); + return model?.changes instanceof Array ? model.changes : Iterable.empty(); + }, + ); + + const sessionFiles = derived(reader => + sessionFileChanges.read(reader).map((entry): IChatCollapsibleListItem => ({ + reference: entry.modifiedUri, + state: ModifiedFileEntryState.Accepted, + kind: 'reference', + options: { + status: undefined, + diffMeta: { added: entry.insertions, removed: entry.deletions }, + originalUri: entry.originalUri, + } + })) + ); + + const shouldRender = derived(reader => editSessionEntries.read(reader).length > 0 || sessionFiles.read(reader).length > 0); this._renderingChatEdits.value = autorun(reader => { if (this.options.renderWorkingSet && shouldRender.read(reader)) { this.renderChatEditingSessionWithEntries( reader.store, - chatEditingSession!, + chatEditingSession, modifiedEntries, - listEntries, + sessionFileChanges, + editSessionEntries, + sessionFiles, ); } else { dom.clearNode(this.chatEditingSessionWidgetContainer); @@ -2039,12 +2070,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } }); } - private renderChatEditingSessionWithEntries( store: DisposableStore, - chatEditingSession: IChatEditingSession, + chatEditingSession: IChatEditingSession | null, modifiedEntries: IObservable, - listEntries: IObservable, + sessionFileChanges: IObservable, + editSessionEntries: IObservable, + sessionEntries: IObservable, ) { // Summary of number of files changed // eslint-disable-next-line no-restricted-syntax @@ -2062,26 +2094,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // eslint-disable-next-line no-restricted-syntax const actionsContainer = overviewRegion.querySelector('.chat-editing-session-actions') as HTMLElement ?? dom.append(overviewRegion, $('.chat-editing-session-actions')); - this._chatEditsActionsDisposables.add(this.instantiationService.createInstance(MenuWorkbenchButtonBar, actionsContainer, MenuId.ChatEditingWidgetToolbar, { - telemetrySource: this.options.menus.telemetrySource, - menuOptions: { - arg: { - $mid: MarshalledId.ChatViewContext, - sessionResource: chatEditingSession.chatSessionResource, - } satisfies IChatViewTitleActionContext, - }, - buttonConfigProvider: (action) => { - if (action.id === ChatEditingShowChangesAction.ID || action.id === ViewPreviousEditsAction.Id) { - return { showIcon: true, showLabel: false, isSecondary: true }; - } - return undefined; - } - })); + const sessionResource = chatEditingSession?.chatSessionResource || this._widget?.viewModel?.model.sessionResource; - if (!chatEditingSession) { - return; + const scopedContextKeyService = this._chatEditsActionsDisposables.add(this.contextKeyService.createScoped(actionsContainer)); + if (sessionResource) { + scopedContextKeyService.createKey(ChatContextKeys.agentSessionType.key, getChatSessionType(sessionResource)); } + this._chatEditsActionsDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, scopedContextKeyService, r => !!sessionEntries.read(r)?.length)); + + const scopedInstantiationService = this._chatEditsActionsDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))); + // Working set // eslint-disable-next-line no-restricted-syntax const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list')); @@ -2092,9 +2115,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ariaLabel: localize('chatEditingSession.toggleWorkingSet', 'Toggle changed files.'), })); - - - store.add(autorun(reader => { + const topLevelStats = derived(reader => { let added = 0; let removed = 0; const entries = modifiedEntries.read(reader); @@ -2104,14 +2125,56 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge removed += entry.linesRemoved.read(reader); } } - const baseLabel = entries.length === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', entries.length); + + let baseLabel = entries.length === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', entries.length); + let shouldShowEditingSession = added > 0 || removed > 0; + let topLevelIsSessionMenu = false; + + if (added === 0 && removed === 0) { + const sessionValue = sessionFileChanges.read(reader) || []; + for (const entry of sessionValue) { + added += entry.insertions; + removed += entry.deletions; + } + + shouldShowEditingSession = sessionValue.length > 0; + baseLabel = sessionValue.length === 1 ? localize('chatEditingSession.oneFile.2', '1 file ready to merge') : localize('chatEditingSession.manyFiles.2', '{0} files ready to merge', sessionValue.length); + topLevelIsSessionMenu = true; + } + button.label = baseLabel; + return { added, removed, shouldShowEditingSession, baseLabel, topLevelIsSessionMenu }; + }); + + const topLevelIsSessionMenu = topLevelStats.map(t => t.topLevelIsSessionMenu); + store.add(autorun(reader => { + const isSessionMenu = topLevelIsSessionMenu.read(reader); + reader.store.add(scopedInstantiationService.createInstance(MenuWorkbenchButtonBar, actionsContainer, isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { + arg: sessionResource && (isSessionMenu ? sessionResource : { + $mid: MarshalledId.ChatViewContext, + sessionResource, + } satisfies IChatViewTitleActionContext), + }, + buttonConfigProvider: (action) => { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ViewPreviousEditsAction.Id || action.id === ViewAllSessionChangesAction.ID) { + return { showIcon: true, showLabel: false, isSecondary: true }; + } + return undefined; + } + })); + })); + + store.add(autorun(reader => { + const { added, removed, shouldShowEditingSession, baseLabel } = topLevelStats.read(reader); + + button.label = baseLabel; this._workingSetLinesAddedSpan.value.textContent = `+${added}`; this._workingSetLinesRemovedSpan.value.textContent = `-${removed}`; button.element.setAttribute('aria-label', localize('chatEditingSession.ariaLabelWithCounts', '{0}, {1} lines added, {2} lines removed', baseLabel, added, removed)); - const shouldShowEditingSession = added > 0 || removed > 0; dom.setVisibility(shouldShowEditingSession, this.chatEditingSessionWidgetContainer); if (!shouldShowEditingSession) { this._onDidChangeHeight.fire(); @@ -2123,18 +2186,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge countsContainer.appendChild(this._workingSetLinesAddedSpan.value); countsContainer.appendChild(this._workingSetLinesRemovedSpan.value); - const applyCollapseState = () => { - button.icon = this._workingSetCollapsed ? Codicon.chevronRight : Codicon.chevronDown; - workingSetContainer.classList.toggle('collapsed', this._workingSetCollapsed); - this._onDidChangeHeight.fire(); - }; - const toggleWorkingSet = () => { - this._workingSetCollapsed = !this._workingSetCollapsed; - applyCollapseState(); + this._workingSetCollapsed.set(!this._workingSetCollapsed.get(), undefined); }; - this._chatEditsActionsDisposables.add(button.onDidClick(() => { toggleWorkingSet(); })); + this._chatEditsActionsDisposables.add(button.onDidClick(toggleWorkingSet)); this._chatEditsActionsDisposables.add(addDisposableListener(overviewRegion, 'click', e => { if (e.defaultPrevented) { return; @@ -2146,7 +2202,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge toggleWorkingSet(); })); - applyCollapseState(); + this._chatEditsActionsDisposables.add(autorun(reader => { + const collapsed = this._workingSetCollapsed.read(reader); + button.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown; + workingSetContainer.classList.toggle('collapsed', collapsed); + this._onDidChangeHeight.fire(); + })); if (!this._chatEditList) { this._chatEditList = this._chatEditsListPool.get(); @@ -2158,8 +2219,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._chatEditsDisposables.add(list.onDidOpen(async (e) => { if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) { const modifiedFileUri = e.element.reference; + const originalUri = e.element.options?.originalUri; - const entry = chatEditingSession.getEntry(modifiedFileUri); + // If there's a originalUri, open as diff editor + if (originalUri) { + await this.editorService.openEditor({ + original: { resource: originalUri }, + modified: { resource: modifiedFileUri }, + options: e.editorOptions + }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + return; + } + + const entry = chatEditingSession?.getEntry(modifiedFileUri); const pane = await this.editorService.openEditor({ resource: modifiedFileUri, @@ -2181,14 +2253,32 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } store.add(autorun(reader => { - const entries = listEntries.read(reader); + const editEntries = editSessionEntries.read(reader); + const sessionFileEntries = sessionEntries.read(reader) ?? []; + + // Combine entries with an optional divider + const allEntries: IChatCollapsibleListItem[] = [...editEntries]; + if (sessionFileEntries.length > 0) { + if (editEntries.length > 0) { + // Add divider between edit session entries and session file entries + allEntries.push({ + kind: 'divider', + label: localize('chatEditingSession.allChanges', 'Worktree Changes'), + menuId: MenuId.ChatEditingSessionChangesToolbar, + menuArg: sessionResource, + scopedInstantiationService, + }); + } + allEntries.push(...sessionFileEntries); + } + const maxItemsShown = 6; - const itemsShown = Math.min(entries.length, maxItemsShown); + const itemsShown = Math.min(allEntries.length, maxItemsShown); const height = itemsShown * 22; const list = this._chatEditList!.object; list.layout(height); list.getHTMLElement().style.height = `${height}px`; - list.splice(0, list.length, entries); + list.splice(0, list.length, allEntries); this._onDidChangeHeight.fire(); })); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts index 1ed5fd59159..7bd794263dc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts @@ -279,10 +279,23 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer('agentSessionsViewerOrientation', undefined, { type: 'number', description: localize('agentSessionsViewerOrientation', "Orientation of the agent sessions view in the chat view.") }); export const agentSessionsViewerPosition = new RawContextKey('agentSessionsViewerPosition', undefined, { type: 'number', description: localize('agentSessionsViewerPosition', "Position of the agent sessions view in the chat view.") }); export const agentSessionType = new RawContextKey('chatSessionType', '', { type: 'string', description: localize('agentSessionType', "The type of the current agent session item.") }); + export const hasAgentSessionChanges = new RawContextKey('chatSessionHasAgentChanges', false, { type: 'boolean', description: localize('chatSessionHasAgentChanges', "True when the current agent session item has changes.") }); export const isArchivedAgentSession = new RawContextKey('agentIsArchived', false, { type: 'boolean', description: localize('agentIsArchived', "True when the agent session item is archived.") }); export const isActiveAgentSession = new RawContextKey('agentIsActive', false, { type: 'boolean', description: localize('agentIsActive', "True when the agent session is currently active (not deletable).") }); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 903f5640599..3dba1490b54 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -122,6 +122,7 @@ export interface IChatContentReference { options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind }; diffMeta?: { added: number; removed: number }; + originalUri?: URI; }; kind: 'reference'; } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index e5e41b76730..15080b6f371 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -73,17 +73,24 @@ export interface IChatSessionItem { startTime: number; endTime?: number; }; - statistics?: { + changes?: { files: number; insertions: number; deletions: number; - }; + } | readonly IChatSessionFileChange[]; archived?: boolean; // TODO:@osortega remove once the single-view is default /** @deprecated */ history?: boolean; } +export interface IChatSessionFileChange { + modifiedUri: URI; + originalUri?: URI; + insertions: number; + deletions: number; +} + export type IChatSessionHistoryItem = { id?: string; type: 'request'; diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts index 6830afe3ba2..863caaea039 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts @@ -191,7 +191,7 @@ suite('Agent Sessions', () => { tooltip: 'Session tooltip', iconPath: ThemeIcon.fromId('check'), timing: { startTime, endTime }, - statistics: { files: 1, insertions: 10, deletions: 5 } + changes: { files: 1, insertions: 10, deletions: 5, details: [] } } ] }; @@ -212,7 +212,7 @@ suite('Agent Sessions', () => { assert.strictEqual(session.status, ChatSessionStatus.Completed); assert.strictEqual(session.timing.startTime, startTime); assert.strictEqual(session.timing.endTime, endTime); - assert.deepStrictEqual(session.statistics, { files: 1, insertions: 10, deletions: 5 }); + assert.deepStrictEqual(session.changes, { files: 1, insertions: 10, deletions: 5 }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts index 5764ed86c21..06253a57c48 100644 --- a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts @@ -532,10 +532,11 @@ suite('LocalAgentsSessionsProvider', () => { const sessions = await provider.provideChatSessionItems(CancellationToken.None); assert.strictEqual(sessions.length, 1); - assert.ok(sessions[0].statistics); - assert.strictEqual(sessions[0].statistics?.files, 2); - assert.strictEqual(sessions[0].statistics?.insertions, 30); - assert.strictEqual(sessions[0].statistics?.deletions, 8); + assert.ok(sessions[0].changes); + const changes = sessions[0].changes as { files: number; insertions: number; deletions: number }; + assert.strictEqual(changes.files, 2); + assert.strictEqual(changes.insertions, 30); + assert.strictEqual(changes.deletions, 8); }); }); @@ -569,7 +570,7 @@ suite('LocalAgentsSessionsProvider', () => { const sessions = await provider.provideChatSessionItems(CancellationToken.None); assert.strictEqual(sessions.length, 1); - assert.strictEqual(sessions[0].statistics, undefined); + assert.strictEqual(sessions[0].changes, undefined); }); }); }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 7a81cfdc79e..6fb6d540e22 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -88,6 +88,8 @@ import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponse import { TestWorkerService } from './testWorkerService.js'; import { MockChatSessionsService } from '../../../chat/test/common/mockChatSessionsService.js'; import { IChatSessionsService } from '../../../chat/common/chatSessionsService.js'; +import { IAgentSessionsService } from '../../../chat/browser/agentSessions/agentSessionsService.js'; +import { IAgentSessionsModel } from '../../../chat/browser/agentSessions/agentSessionsModel.js'; suite('InlineChatController', function () { @@ -236,6 +238,17 @@ suite('InlineChatController', function () { }], [IChatEntitlementService, new SyncDescriptor(TestChatEntitlementService)], [IChatSessionsService, new SyncDescriptor(MockChatSessionsService)], + [IAgentSessionsService, new class extends mock() { + override get model(): IAgentSessionsModel { + return { + onWillResolve: Event.None, + onDidResolve: Event.None, + onDidChangeSessions: Event.None, + sessions: [], + resolve: async () => { } + } as IAgentSessionsModel; + } + }], ); instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 5baeec594dd..cae833acee0 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -467,6 +467,12 @@ const apiMenus: IAPIMenu[] = [ supportsSubmenus: false, proposed: 'chatParticipantPrivate' }, + { + key: 'chat/input/editing/sessionToolbar', + id: MenuId.ChatEditingSessionChangesToolbar, + description: localize('menus.chatEditingSessionChangesToolbar', "The Chat Editing widget toolbar menu for session changes."), + proposed: 'chatSessionsProvider' + }, { // TODO: rename this to something like: `chatSessions/item/inline` key: 'chat/chatSessions', diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 7cd69e208e5..bd4e624430f 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -122,7 +122,7 @@ declare module 'vscode' { /** * Statistics about the chat session. */ - statistics?: { + changes?: readonly ChatSessionChangedFile[] | { /** * Number of files edited during the session. */ @@ -140,6 +140,30 @@ declare module 'vscode' { }; } + export class ChatSessionChangedFile { + /** + * URI of the file. + */ + modifiedUri: Uri; + + /** + * File opened when the user takes the 'compare' action. + */ + originalUri?: Uri; + + /** + * Number of insertions made during the session. + */ + insertions: number; + + /** + * Number of deletions made during the session. + */ + deletions: number; + + constructor(modifiedUri: Uri, insertions: number, deletions: number, originalUri?: Uri); + } + export interface ChatSession { /** * The full history of the session