Skip to content

Commit c43f508

Browse files
committed
feat: render edit generation progress in chat editing widget
1 parent 037dadc commit c43f508

File tree

3 files changed

+75
-49
lines changed

3 files changed

+75
-49
lines changed

src/vs/workbench/contrib/chat/browser/chatEditingService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,7 @@ class ChatEditingSession extends Disposable implements IChatEditingSession {
721721

722722
private async _resolve(): Promise<void> {
723723
this._state.set(ChatEditingSessionState.Idle, undefined);
724+
this._onDidChange.fire();
724725
}
725726

726727
private async _getOrCreateModifiedFileEntry(resource: URI): Promise<ModifiedFileEntry> {

src/vs/workbench/contrib/chat/browser/chatInputPart.ts

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Button } from '../../../../base/browser/ui/button/button.js';
1212
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
1313
import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
1414
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
15+
import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js';
1516
import { IAction } from '../../../../base/common/actions.js';
1617
import { Codicon } from '../../../../base/common/codicons.js';
1718
import { Emitter, Event } from '../../../../base/common/event.js';
@@ -61,7 +62,7 @@ import { AccessibilityCommandId } from '../../accessibility/common/accessibility
6162
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js';
6263
import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js';
6364
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';
64-
import { IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js';
65+
import { ChatEditingSessionState, IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js';
6566
import { IChatRequestVariableEntry } from '../common/chatModel.js';
6667
import { IChatFollowup } from '../common/chatService.js';
6768
import { IChatResponseViewModel } from '../common/chatViewModel.js';
@@ -199,6 +200,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
199200
readonly inputUri = URI.parse(`${ChatInputPart.INPUT_SCHEME}:input-${ChatInputPart._counter++}`);
200201

201202
private readonly _chatEditsDisposables = this._register(new DisposableStore());
203+
private _chatEditsProgress: ProgressBar | undefined;
202204
private _chatEditsListPool: CollapsibleListPool;
203205
private _chatEditList: IDisposableReference<WorkbenchList<IChatCollapsibleListItem>> | undefined;
204206
get selectedElements(): URI[] {
@@ -839,30 +841,40 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
839841
}
840842

841843
async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null) {
842-
dom.clearNode(this.chatEditingSessionWidgetContainer);
843844
dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer);
844-
this._chatEditsDisposables.clear();
845-
this._chatEditList = undefined;
845+
846846
if (!chatEditingSession) {
847+
dom.clearNode(this.chatEditingSessionWidgetContainer);
848+
this._chatEditsDisposables.clear();
849+
this._chatEditList = undefined;
850+
this._chatEditsProgress?.dispose();
851+
this._chatEditsProgress = undefined;
847852
return;
848853
}
849854

850-
const innerContainer = dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons'));
855+
if (this._chatEditList && chatEditingSession.state.get() === ChatEditingSessionState.Idle) {
856+
this._chatEditsProgress?.stop();
857+
return;
858+
}
851859

852860
// Summary of number of files changed
861+
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'));
853862
const editedFiles = chatEditingSession.entries.get();
854863
const numberOfEditedEntries = editedFiles.length;
855-
const overviewRegion = dom.append(innerContainer, $('.chat-editing-session-overview'));
856-
const overviewText = dom.append(overviewRegion, $('span'));
857-
overviewText.textContent = numberOfEditedEntries === 1
858-
? localize('chatEditingSessionOverview.oneFileChanged', "1 file changed")
859-
: localize('chatEditingSessionOverview', "{0} files changed", numberOfEditedEntries);
864+
const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview'));
865+
if (numberOfEditedEntries !== this._chatEditList?.object.length) {
866+
const overviewText = overviewRegion.querySelector('span') ?? dom.append(overviewRegion, $('span'));
867+
overviewText.textContent = numberOfEditedEntries === 1
868+
? localize('chatEditingSessionOverview.oneFileChanged', "1 file changed")
869+
: localize('chatEditingSessionOverview', "{0} files changed", numberOfEditedEntries);
870+
}
860871

861872
// Chat editing session actions
862-
const actionsContainer = dom.append(overviewRegion, $('.chat-editing-session-actions'));
873+
let actionsContainer = overviewRegion.querySelector('.chat-editing-session-actions') as HTMLElement;
863874
const actions = [];
864-
// Don't show Accept All / Discard All actions if user already selected Accept All / Discard All
865-
if (editedFiles.find((e) => e.state.get() === ModifiedFileEntryState.Undecided)) {
875+
if (!actionsContainer && editedFiles.find((e) => e.state.get() === ModifiedFileEntryState.Undecided)) {
876+
// Don't show Accept All / Discard All actions if user already selected Accept All / Discard All
877+
actionsContainer = dom.append(overviewRegion, $('.chat-editing-session-actions'));
866878
actions.push(
867879
{
868880
command: ChatEditingShowChangesAction.ID,
@@ -880,60 +892,68 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
880892
isSecondary: false
881893
}
882894
);
883-
}
884895

885-
for (const action of actions) {
886-
const button = this._register(new Button(actionsContainer, {
887-
supportIcons: false,
888-
secondary: action.isSecondary
889-
}));
890-
button.label = action.label;
891-
this._register(button.onDidClick(() => {
892-
this.commandService.executeCommand(action.command);
893-
}));
894-
dom.append(actionsContainer, button.element);
896+
for (const action of actions) {
897+
const button = this._register(new Button(actionsContainer, {
898+
supportIcons: false,
899+
secondary: action.isSecondary
900+
}));
901+
button.label = action.label;
902+
this._register(button.onDidClick(() => {
903+
this.commandService.executeCommand(action.command);
904+
}));
905+
dom.append(actionsContainer, button.element);
906+
}
907+
908+
const clearButton = new Button(actionsContainer, { supportIcons: true });
909+
this._chatEditsDisposables.add(clearButton);
910+
clearButton.icon = Codicon.close;
911+
const disp = clearButton.onDidClick((e) => {
912+
disp.dispose();
913+
chatEditingSession.dispose();
914+
});
915+
dom.append(actionsContainer, clearButton.element);
895916
}
896917

897-
const clearButton = new Button(actionsContainer, { supportIcons: true });
898-
this._chatEditsDisposables.add(clearButton);
899-
clearButton.icon = Codicon.close;
900-
const disp = clearButton.onDidClick((e) => {
901-
disp.dispose();
902-
chatEditingSession.dispose();
903-
});
904-
dom.append(actionsContainer, clearButton.element);
918+
if (!this._chatEditsProgress && chatEditingSession.state.get() === ChatEditingSessionState.StreamingEdits) {
919+
this._chatEditsProgress = new ProgressBar(innerContainer);
920+
this._chatEditsProgress.infinite().show(500);
921+
}
905922

906923
// List of edited files
907-
if (!editedFiles.length) {
924+
if (!editedFiles.length || editedFiles.length === this._chatEditList?.object.length) {
908925
return;
909926
}
910927
const entries: IChatCollapsibleListItem[] = editedFiles.map((entry) => ({
911928
reference: entry.modifiedURI,
912929
kind: 'reference',
913930
}));
914-
const editedFilesList = this._chatEditsListPool.get();
915-
this._chatEditList = editedFilesList;
916-
const list = editedFilesList.object;
917-
this._chatEditsDisposables.add(editedFilesList);
918-
this._chatEditsDisposables.add(list.onDidOpen((e) => {
919-
if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) {
920-
const modifiedFileUri = e.element.reference;
921-
const editedFile = editedFiles.find((e) => e.modifiedURI.toString() === modifiedFileUri.toString());
922-
if (editedFile) {
923-
void this.editorService.openEditor({
924-
original: { resource: URI.from(editedFile.originalURI, true) },
925-
modified: { resource: URI.from(editedFile.modifiedURI, true) },
926-
});
931+
if (!this._chatEditList) {
932+
this._chatEditList = this._chatEditsListPool.get();
933+
const list = this._chatEditList.object;
934+
this._chatEditsDisposables.add(this._chatEditList);
935+
this._chatEditsDisposables.add(list.onDidOpen((e) => {
936+
if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) {
937+
const modifiedFileUri = e.element.reference;
938+
const editedFile = editedFiles.find((e) => e.modifiedURI.toString() === modifiedFileUri.toString());
939+
if (editedFile) {
940+
void this.editorService.openEditor({
941+
original: { resource: URI.from(editedFile.originalURI, true) },
942+
modified: { resource: URI.from(editedFile.modifiedURI, true) },
943+
});
944+
}
927945
}
928-
}
929-
}));
946+
}));
947+
dom.append(innerContainer, list.getHTMLElement());
948+
}
949+
930950
const maxItemsShown = 6;
931951
const itemsShown = Math.min(numberOfEditedEntries, maxItemsShown);
932952
const height = itemsShown * 22;
953+
const list = this._chatEditList.object;
933954
list.layout(height);
934955
list.getHTMLElement().style.height = `${height}px`;
935956
list.splice(0, list.length, entries);
936-
dom.append(innerContainer, editedFilesList.object.getHTMLElement());
937957
}
938958

939959
async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise<void> {

src/vs/workbench/contrib/chat/browser/media/chat.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,12 +524,17 @@ have to be updated for changes to the rules above, or to support more deeply nes
524524
font-size: 11px;
525525
}
526526

527+
.interactive-session .chat-editing-session .chat-editing-session-container .monaco-progress-container {
528+
position: relative;
529+
}
530+
527531
.interactive-session .chat-editing-session .chat-editing-session-actions {
528532
display: flex;
529533
flex-direction: row;
530534
flex-wrap: wrap;
531535
gap: 6px;
532536
align-items: center;
537+
justify-content: end;
533538
}
534539

535540
.interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button {

0 commit comments

Comments
 (0)