From c43f508b732d24b0c4732de9db2b38b4c5b88b8a Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 26 Sep 2024 06:03:39 +0200 Subject: [PATCH] feat: render edit generation progress in chat editing widget --- .../chat/browser/chatEditingService.ts | 1 + .../contrib/chat/browser/chatInputPart.ts | 118 ++++++++++-------- .../contrib/chat/browser/media/chat.css | 5 + 3 files changed, 75 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditingService.ts index 455fecb0d8f..bf5febb538f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditingService.ts @@ -721,6 +721,7 @@ class ChatEditingSession extends Disposable implements IChatEditingSession { private async _resolve(): Promise { this._state.set(ChatEditingSessionState.Idle, undefined); + this._onDidChange.fire(); } private async _getOrCreateModifiedFileEntry(resource: URI): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d42b4f16e40..a7a37c48c9d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -12,6 +12,7 @@ import { Button } from '../../../../base/browser/ui/button/button.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; import { IAction } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; @@ -61,7 +62,7 @@ import { AccessibilityCommandId } from '../../accessibility/common/accessibility import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from '../common/chatContextKeys.js'; -import { IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js'; +import { ChatEditingSessionState, IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; @@ -199,6 +200,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly inputUri = URI.parse(`${ChatInputPart.INPUT_SCHEME}:input-${ChatInputPart._counter++}`); private readonly _chatEditsDisposables = this._register(new DisposableStore()); + private _chatEditsProgress: ProgressBar | undefined; private _chatEditsListPool: CollapsibleListPool; private _chatEditList: IDisposableReference> | undefined; get selectedElements(): URI[] { @@ -839,30 +841,40 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null) { - dom.clearNode(this.chatEditingSessionWidgetContainer); dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); - this._chatEditsDisposables.clear(); - this._chatEditList = undefined; + if (!chatEditingSession) { + dom.clearNode(this.chatEditingSessionWidgetContainer); + this._chatEditsDisposables.clear(); + this._chatEditList = undefined; + this._chatEditsProgress?.dispose(); + this._chatEditsProgress = undefined; return; } - const innerContainer = dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); + if (this._chatEditList && chatEditingSession.state.get() === ChatEditingSessionState.Idle) { + this._chatEditsProgress?.stop(); + return; + } // Summary of number of files changed + const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); const editedFiles = chatEditingSession.entries.get(); const numberOfEditedEntries = editedFiles.length; - const overviewRegion = dom.append(innerContainer, $('.chat-editing-session-overview')); - const overviewText = dom.append(overviewRegion, $('span')); - overviewText.textContent = numberOfEditedEntries === 1 - ? localize('chatEditingSessionOverview.oneFileChanged', "1 file changed") - : localize('chatEditingSessionOverview', "{0} files changed", numberOfEditedEntries); + const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview')); + if (numberOfEditedEntries !== this._chatEditList?.object.length) { + const overviewText = overviewRegion.querySelector('span') ?? dom.append(overviewRegion, $('span')); + overviewText.textContent = numberOfEditedEntries === 1 + ? localize('chatEditingSessionOverview.oneFileChanged', "1 file changed") + : localize('chatEditingSessionOverview', "{0} files changed", numberOfEditedEntries); + } // Chat editing session actions - const actionsContainer = dom.append(overviewRegion, $('.chat-editing-session-actions')); + let actionsContainer = overviewRegion.querySelector('.chat-editing-session-actions') as HTMLElement; const actions = []; - // Don't show Accept All / Discard All actions if user already selected Accept All / Discard All - if (editedFiles.find((e) => e.state.get() === ModifiedFileEntryState.Undecided)) { + if (!actionsContainer && editedFiles.find((e) => e.state.get() === ModifiedFileEntryState.Undecided)) { + // Don't show Accept All / Discard All actions if user already selected Accept All / Discard All + actionsContainer = dom.append(overviewRegion, $('.chat-editing-session-actions')); actions.push( { command: ChatEditingShowChangesAction.ID, @@ -880,60 +892,68 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge isSecondary: false } ); + + for (const action of actions) { + const button = this._register(new Button(actionsContainer, { + supportIcons: false, + secondary: action.isSecondary + })); + button.label = action.label; + this._register(button.onDidClick(() => { + this.commandService.executeCommand(action.command); + })); + dom.append(actionsContainer, button.element); + } + + const clearButton = new Button(actionsContainer, { supportIcons: true }); + this._chatEditsDisposables.add(clearButton); + clearButton.icon = Codicon.close; + const disp = clearButton.onDidClick((e) => { + disp.dispose(); + chatEditingSession.dispose(); + }); + dom.append(actionsContainer, clearButton.element); } - for (const action of actions) { - const button = this._register(new Button(actionsContainer, { - supportIcons: false, - secondary: action.isSecondary - })); - button.label = action.label; - this._register(button.onDidClick(() => { - this.commandService.executeCommand(action.command); - })); - dom.append(actionsContainer, button.element); + if (!this._chatEditsProgress && chatEditingSession.state.get() === ChatEditingSessionState.StreamingEdits) { + this._chatEditsProgress = new ProgressBar(innerContainer); + this._chatEditsProgress.infinite().show(500); } - const clearButton = new Button(actionsContainer, { supportIcons: true }); - this._chatEditsDisposables.add(clearButton); - clearButton.icon = Codicon.close; - const disp = clearButton.onDidClick((e) => { - disp.dispose(); - chatEditingSession.dispose(); - }); - dom.append(actionsContainer, clearButton.element); - // List of edited files - if (!editedFiles.length) { + if (!editedFiles.length || editedFiles.length === this._chatEditList?.object.length) { return; } const entries: IChatCollapsibleListItem[] = editedFiles.map((entry) => ({ reference: entry.modifiedURI, kind: 'reference', })); - const editedFilesList = this._chatEditsListPool.get(); - this._chatEditList = editedFilesList; - const list = editedFilesList.object; - this._chatEditsDisposables.add(editedFilesList); - this._chatEditsDisposables.add(list.onDidOpen((e) => { - if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) { - const modifiedFileUri = e.element.reference; - const editedFile = editedFiles.find((e) => e.modifiedURI.toString() === modifiedFileUri.toString()); - if (editedFile) { - void this.editorService.openEditor({ - original: { resource: URI.from(editedFile.originalURI, true) }, - modified: { resource: URI.from(editedFile.modifiedURI, true) }, - }); + if (!this._chatEditList) { + this._chatEditList = this._chatEditsListPool.get(); + const list = this._chatEditList.object; + this._chatEditsDisposables.add(this._chatEditList); + this._chatEditsDisposables.add(list.onDidOpen((e) => { + if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) { + const modifiedFileUri = e.element.reference; + const editedFile = editedFiles.find((e) => e.modifiedURI.toString() === modifiedFileUri.toString()); + if (editedFile) { + void this.editorService.openEditor({ + original: { resource: URI.from(editedFile.originalURI, true) }, + modified: { resource: URI.from(editedFile.modifiedURI, true) }, + }); + } } - } - })); + })); + dom.append(innerContainer, list.getHTMLElement()); + } + const maxItemsShown = 6; const itemsShown = Math.min(numberOfEditedEntries, 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); - dom.append(innerContainer, editedFilesList.object.getHTMLElement()); } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 0e72a0de1de..70fdab62ead 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -524,12 +524,17 @@ have to be updated for changes to the rules above, or to support more deeply nes font-size: 11px; } +.interactive-session .chat-editing-session .chat-editing-session-container .monaco-progress-container { + position: relative; +} + .interactive-session .chat-editing-session .chat-editing-session-actions { display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px; align-items: center; + justify-content: end; } .interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button {