From 6f7f8f5d620e2d1ae279b620e9589c458b9e8806 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 21 Apr 2023 17:27:57 -0700 Subject: [PATCH 1/2] Change the way the InteractiveSessionModel is loaded into the widget And the way viewState is managed. Fixes a bunch of general issues with this flow between views and editors --- .../actions/interactiveSessionActions.ts | 8 +- .../interactiveSessionInputEditorContrib.ts | 2 +- .../browser/interactiveSession.ts | 1 - .../browser/interactiveSessionEditor.ts | 82 +++++++++---------- .../browser/interactiveSessionInputPart.ts | 26 ++++-- .../browser/interactiveSessionViewPane.ts | 58 ++++++++++--- .../browser/interactiveSessionWidget.ts | 48 ++++------- .../common/interactiveSessionModel.ts | 14 ++-- .../common/interactiveSessionViewModel.ts | 5 ++ 9 files changed, 144 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionActions.ts b/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionActions.ts index 966ddb04ef74d..52599699e8c6f 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionActions.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/actions/interactiveSessionActions.ts @@ -8,7 +8,6 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -// import { CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; import { localize } from 'vs/nls'; import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -20,6 +19,7 @@ import { InteractiveSessionEditorInput } from 'vs/workbench/contrib/interactiveS import { InteractiveSessionViewPane } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionViewPane'; import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; import { CONTEXT_IN_INTERACTIVE_INPUT, CONTEXT_IN_INTERACTIVE_SESSION } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContextKeys'; +import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import { IInteractiveSessionWidgetHistoryService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWidgetHistoryService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -155,7 +155,11 @@ export function registerInteractiveSessionActions() { } async run(accessor: ServicesAccessor, ...args: any[]) { const widgetService = accessor.get(IInteractiveSessionWidgetService); - await widgetService.lastFocusedWidget?.clear(); + const interactiveSessionService = accessor.get(IInteractiveSessionService); + const sessionId = widgetService.lastFocusedWidget?.viewModel?.sessionId; + if (sessionId) { + interactiveSessionService.clearSession(sessionId); + } } }); } diff --git a/src/vs/workbench/contrib/interactiveSession/browser/contrib/interactiveSessionInputEditorContrib.ts b/src/vs/workbench/contrib/interactiveSession/browser/contrib/interactiveSessionInputEditorContrib.ts index 968fa4f85eb8c..5b459f17c325f 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/contrib/interactiveSessionInputEditorContrib.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/contrib/interactiveSessionInputEditorContrib.ts @@ -62,7 +62,7 @@ class InputEditorDecorations extends Disposable { private async updateInputEditorDecorations() { const value = this.widget.inputEditor.getValue(); - const slashCommands = await this.widget.getSlashCommands(); + const slashCommands = await this.widget.getSlashCommands(); // TODO this async call can lead to a flicker of the placeholder text when switching editor tabs if (!value) { const extensionPlaceholder = this.widget.viewModel?.inputPlaceholder; diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSession.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSession.ts index 7764260d5b015..6cb5ee6231c92 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSession.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSession.ts @@ -15,7 +15,6 @@ export interface IInteractiveSessionWidget { readonly providerId: string; acceptInput(query?: string): void; - clear(): void; focusLastMessage(): void; focusInput(): void; getSlashCommands(): Promise; diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts index 94e1e12dfd6c9..a490db7c2abc8 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts @@ -5,12 +5,11 @@ import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -19,7 +18,8 @@ import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { Memento } from 'vs/workbench/common/memento'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { InteractiveSessionEditorInput } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditorInput'; -import { InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; +import { IViewState, InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; +import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; export interface IInteractiveSessionEditorOptions extends IEditorOptions { providerId: string; @@ -28,35 +28,47 @@ export interface IInteractiveSessionEditorOptions extends IEditorOptions { export class InteractiveSessionEditor extends EditorPane { static readonly ID: string = 'workbench.editor.interactiveSession'; - private widget: InteractiveSessionWidget | undefined; - private widgetDisposables = this._register(new DisposableStore()); + private widget!: InteractiveSessionWidget; - private parentElement: HTMLElement | undefined; - private dimension: dom.Dimension | undefined; - - private readonly _scopedContextKeyService = this._register(new MutableDisposable()); + private _scopedContextKeyService!: IScopedContextKeyService; override get scopedContextKeyService() { - return this._scopedContextKeyService.value; + return this._scopedContextKeyService; } + private _memento: Memento | undefined; + private _viewState: IViewState | undefined; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService, ) { super(InteractiveSessionEditor.ID, telemetryService, themeService, storageService); } public async clear() { - if (this.widget) { - await this.widget.clear(); + if (this.widget?.viewModel) { + this.interactiveSessionService.clearSession(this.widget.viewModel.sessionId); } } protected override createEditor(parent: HTMLElement): void { - this.parentElement = parent; + this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); + const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + + this.widget = this._register( + scopedInstantiationService.createInstance(InteractiveSessionWidget, { resource: true }, () => editorBackground, () => SIDE_BAR_BACKGROUND, () => SIDE_BAR_BACKGROUND)); + this._register(this.widget.onDidChangeViewModel(() => { + // TODO replace with listening for model disposal + // This part is a bit odd. The widget's session and model will change. When that happens, store the latest session id + // on the EditorInput so that it can be restored when the editor moves or the window reloads. + (this.input! as InteractiveSessionEditorInput).sessionId = this.widget!.viewModel?.sessionId; + })); + this.widget.render(parent); + this.widget.setVisible(true); } public override focus(): void { @@ -65,46 +77,36 @@ export class InteractiveSessionEditor extends EditorPane { } } + override clearInput(): void { + this.saveState(); + super.clearInput(); + } + override async setInput(input: InteractiveSessionEditorInput, options: IInteractiveSessionEditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { super.setInput(input, options, context, token); - this.widgetDisposables.clear(); - const editorModel = await input.resolve(); if (!editorModel) { throw new Error(`Failed to get model for interactive session editor. id: ${input.sessionId}`); } - if (!this.parentElement) { - throw new Error('InteractiveSessionEditor lifecycle issue: Parent element not set'); + if (!this.widget) { + throw new Error('InteractiveSessionEditor lifecycle issue: no editor widget'); } - this._scopedContextKeyService.value = this._register(this.contextKeyService.createScoped(this.parentElement)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - - if (this.widget) { - dom.clearNode(this.parentElement); - } - - const memento = new Memento(input.resource.path, this.storageService); - this.widget = this.widgetDisposables.add( - scopedInstantiationService.createInstance(InteractiveSessionWidget, editorModel.model.providerId, editorModel.model, { resource: input.resource }, () => editorBackground, () => SIDE_BAR_BACKGROUND, () => SIDE_BAR_BACKGROUND, memento)); - this.widget.render(this.parentElement); - this.widget.setVisible(true); - - this.widgetDisposables.add(this.widget.onDidChangeViewModel(() => { - // This part is a bit odd. The widget's session and model will change. When that happens, store the latest session id - // on the EditorInput so that it can be restored when the editor moves or the window reloads. - input.sessionId = this.widget!.viewModel?.sessionId; - })); - - if (this.dimension) { - this.layout(this.dimension, undefined); - } + this._memento = new Memento('interactive-session-editor-' + editorModel.model.sessionId, this.storageService); + this._viewState = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER) as IViewState; + this.widget.setModel(editorModel.model, { ...this._viewState }); } protected override saveState(): void { this.widget?.saveState(); + + if (this._memento && this._viewState) { + const widgetViewState = this.widget.getViewState(); + this._viewState!.inputValue = widgetViewState.inputValue; + this._memento!.saveMemento(); + } } override layout(dimension: dom.Dimension, position?: dom.IDomPosition | undefined): void { @@ -112,8 +114,6 @@ export class InteractiveSessionEditor extends EditorPane { const width = Math.min(dimension.width, 600); this.widget.layout(dimension.height, width); } - - this.dimension = dimension; } } diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionInputPart.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionInputPart.ts index 2bf2915bfeff3..695f2f6ac3806 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionInputPart.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionInputPart.ts @@ -65,11 +65,11 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN private setHistoryNavigationEnablement!: (enabled: boolean) => void; private inputModel: ITextModel | undefined; private inputEditorHasText: IContextKey; + private providerId: string | undefined; public readonly inputUri = URI.parse(`${InteractiveSessionInputPart.INPUT_SCHEME}:input-${InteractiveSessionInputPart._counter++}`); constructor( - private readonly providerId: string, // private readonly editorOptions: InteractiveSessionEditorOptions, // TODO this should be used @IInteractiveSessionWidgetHistoryService private readonly historyService: IInteractiveSessionWidgetHistoryService, @IModelService private readonly modelService: IModelService, @@ -79,10 +79,16 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN super(); this.inputEditorHasText = CONTEXT_INTERACTIVE_INPUT_HAS_TEXT.bindTo(contextKeyService); + this.history = new HistoryNavigator([], 5); + this._register(this.historyService.onDidClearHistory(() => this.history.clear())); + } - const history = this.historyService.getHistory(this.providerId); + setState(providerId: string, inputValue: string): void { + this.providerId = providerId; + const history = this.historyService.getHistory(providerId); this.history = new HistoryNavigator(history, 50); - this._register(this.historyService.onDidClearHistory(() => this.history.clear())); + + this.setValue(inputValue); } get element(): HTMLElement { @@ -102,15 +108,17 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN (this.history.previous() ?? this.history.first()) : this.history.next()) ?? ''; - this.inputEditor.setValue(historyInput); aria.status(historyInput); - if (historyInput) { - // always leave cursor at the end. - this.inputEditor.setPosition({ lineNumber: 1, column: historyInput.length + 1 }); - } + this.setValue(historyInput); this.setHistoryNavigationEnablement(true); } + private setValue(value: string): void { + this.inputEditor.setValue(value); + // always leave cursor at the end + this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); + } + focus() { this._inputEditor.focus(); } @@ -240,6 +248,6 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN saveState(): void { const inputHistory = this.history.getHistory(); - this.historyService.saveHistory(this.providerId, inputHistory); + this.historyService.saveHistory(this.providerId!, inputHistory); } } diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionViewPane.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionViewPane.ts index 6fed39b60815d..067883e8b1b8f 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionViewPane.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionViewPane.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -10,14 +12,15 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { Memento } from 'vs/workbench/common/memento'; import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; +import { IViewState, InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; +import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; export interface IInteractiveSessionViewOptions { readonly providerId: string; @@ -27,11 +30,15 @@ export const INTERACTIVE_SIDEBAR_PANEL_ID = 'workbench.panel.interactiveSessionS export class InteractiveSessionViewPane extends ViewPane { static ID = 'workbench.panel.interactiveSession.view'; - private _widget: InteractiveSessionWidget; + private _widget!: InteractiveSessionWidget; get widget(): InteractiveSessionWidget { return this._widget; } + private modelDisposables = this._register(new DisposableStore()); + private memento: Memento; + private viewState: IViewState; + constructor( - interactiveSessionViewOptions: IInteractiveSessionViewOptions, + private readonly interactiveSessionViewOptions: IInteractiveSessionViewOptions, options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @@ -42,22 +49,42 @@ export class InteractiveSessionViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, + @IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - const memento = new Memento('interactive-session-' + interactiveSessionViewOptions.providerId, storageService); - this._widget = this._register(scopedInstantiationService.createInstance(InteractiveSessionWidget, interactiveSessionViewOptions.providerId, undefined, { viewId: this.id }, () => this.getBackgroundColor(), () => this.getBackgroundColor(), () => editorBackground, memento)); + // View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento. + this.memento = new Memento('interactive-session-view-' + this.interactiveSessionViewOptions.providerId, this.storageService); + this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER) as IViewState; + } - this._register(this.onDidChangeBodyVisibility(visible => { - this._widget.setVisible(visible); + private updateModel(initial = false): void { + this.modelDisposables.clear(); + + const model = this.interactiveSessionService.startSession(this.interactiveSessionViewOptions.providerId, initial, CancellationToken.None); + if (!model) { + throw new Error('Could not start interactive session'); + } + + this._widget.setModel(model, { ...this.viewState }); + this.modelDisposables.add(model.onDidDispose(() => { + this.updateModel(); })); } protected override renderBody(parent: HTMLElement): void { super.renderBody(parent); + + const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + + this._widget = this._register(scopedInstantiationService.createInstance(InteractiveSessionWidget, { viewId: this.id }, () => this.getBackgroundColor(), () => this.getBackgroundColor(), () => editorBackground)); + this._register(this.onDidChangeBodyVisibility(visible => { + this._widget.setVisible(visible); + })); this._widget.render(parent); + + this.updateModel(true); } acceptInput(query?: string): void { @@ -65,7 +92,9 @@ export class InteractiveSessionViewPane extends ViewPane { } async clear(): Promise { - await this._widget.clear(); + if (this.widget.viewModel) { + this.interactiveSessionService.clearSession(this.widget.viewModel.sessionId); + } } focusInput(): void { @@ -83,7 +112,14 @@ export class InteractiveSessionViewPane extends ViewPane { } override saveState(): void { + // Since input history is per-provider, this is handled by a separate service and not the memento here. + // TODO multiple chat views will overwrite each other this._widget.saveState(); + + const widgetViewState = this._widget.getViewState(); + this.viewState.inputValue = widgetViewState.inputValue; + this.memento.saveMemento(); + super.saveState(); } } diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts index 9802eb5b7c506..0256c56e65505 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget.ts @@ -19,9 +19,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { foreground } from 'vs/platform/theme/common/colorRegistry'; -import { Memento } from 'vs/workbench/common/memento'; import { IViewsService } from 'vs/workbench/common/views'; import { IInteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSession'; import { InteractiveSessionInputPart } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionInputPart'; @@ -59,12 +57,12 @@ function revealLastElement(list: WorkbenchObjectTree) { list.scrollTop = list.scrollHeight - list.renderHeight; } -interface IViewState { - inputValue: string; +export interface IViewState { + inputValue?: string; // renderData } -export type IInteractiveSessionWidgetViewContext = { viewId: string } | { resource: URI }; +export type IInteractiveSessionWidgetViewContext = { viewId: string } | { resource: boolean }; export class InteractiveSessionWidget extends Disposable implements IInteractiveSessionWidget { public static readonly CONTRIBS: { new(...args: [IInteractiveSessionWidget, ...any]): any }[] = []; @@ -121,16 +119,11 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive private lastSlashCommands: IInteractiveSlashCommand[] | undefined; private slashCommandsPromise: Promise | undefined; - private viewState: IViewState; - constructor( - readonly providerId: string, - initialModel: IInteractiveSessionModel | undefined, readonly viewContext: IInteractiveSessionWidgetViewContext, private readonly listBackgroundColorDelegate: () => string, private readonly inputEditorBackgroundColorDelegate: () => string, private readonly resultEditorBackgroundColorDelegate: () => string, - private readonly memento: Memento, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService, @@ -142,9 +135,10 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive this.requestInProgress = CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS.bindTo(contextKeyService); this._register((interactiveSessionWidgetService as InteractiveSessionWidgetService).register(this)); - this.initializeSessionModel(true, initialModel); + } - this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER) as IViewState; + get providerId(): string { + return this.viewModel?.providerId || ''; } get inputEditor(): ICodeEditor { @@ -344,8 +338,8 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive } private createInput(container: HTMLElement): void { - this.inputPart = this.instantiationService.createInstance(InteractiveSessionInputPart, this.providerId); - this.inputPart.render(container, this.viewState.inputValue, this); + this.inputPart = this.instantiationService.createInstance(InteractiveSessionInputPart); + this.inputPart.render(container, '', this); this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire())); this._register(this.inputPart.onDidAcceptFollowup(followup => this.acceptInput(followup))); @@ -356,10 +350,9 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? ''); } - private initializeSessionModel(initial = false, initialModel?: IInteractiveSessionModel | undefined) { - const model = initialModel ?? this.interactiveSessionService.startSession(this.providerId, initial, CancellationToken.None); - if (!model) { - throw new Error('Failed to start session'); + setModel(model: IInteractiveSessionModel, viewState: IViewState): void { + if (!this.container) { + throw new Error('Call render() before setModel()'); } this.viewModel = this.instantiationService.createInstance(InteractiveSessionViewModel, model); @@ -369,9 +362,11 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive this.onDidChangeItems(); })); this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => { + // Disposes the viewmodel and listeners this.viewModel = undefined; this.onDidChangeItems(); })); + this.inputPart.setState(model.providerId, viewState.inputValue ?? ''); if (this.tree) { this.onDidChangeItems(); @@ -386,7 +381,7 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive // Shortcut for /clear command if (!query && editorValue.trim() === '/clear') { // If this becomes a repeated pattern, we should have a real internal slash command provider system - this.clear(); + this.interactiveSessionService.clearSession(this.viewModel.sessionId); this.inputPart.inputEditor.setValue(''); return; } @@ -415,14 +410,6 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive this.tree.domFocus(); } - async clear(): Promise { - if (this.viewModel) { - this.interactiveSessionService.clearSession(this.viewModel.sessionId); - this.initializeSessionModel(); - this.focusInput(); - } - } - layout(height: number, width: number): void { this.bodyDimension = new dom.Dimension(width, height); @@ -443,13 +430,14 @@ export class InteractiveSessionWidget extends Disposable implements IInteractive saveState(): void { this.inputPart.saveState(); + } - this.viewState.inputValue = this.inputPart.inputEditor.getValue(); - this.memento.saveMemento(); + getViewState(): IViewState { + this.inputPart.saveState(); + return { inputValue: this.inputPart.inputEditor.getValue() }; } public override dispose(): void { - this.saveState(); super.dispose(); if (this.viewModel) { diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts index 10ac36258165d..57e6878c1f8e0 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts @@ -101,10 +101,14 @@ export class InteractiveResponseModel extends Disposable implements IInteractive return this._errorDetails; } + public get providerId(): string { + return this._session.providerId; + } + constructor( private _response: IMarkdownString, + private readonly _session: InteractiveSessionModel, public readonly username: string, - public readonly providerId: string, public readonly avatarIconUri?: URI, private _isComplete: boolean = false, private _isCanceled = false, @@ -265,7 +269,7 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS return requests.map((raw: ISerializableInteractiveSessionRequestData) => { const request = new InteractiveRequestModel(raw.message, obj.requesterUsername, obj.requesterAvatarIconUri && URI.revive(obj.requesterAvatarIconUri)); if (raw.response || raw.responseErrorDetails) { - request.response = new InteractiveResponseModel(new MarkdownString(raw.response), obj.responderUsername, this.providerId, obj.responderAvatarIconUri && URI.revive(obj.responderAvatarIconUri), true, raw.isCanceled, raw.vote, raw.providerResponseId, raw.responseErrorDetails, raw.followups); + request.response = new InteractiveResponseModel(new MarkdownString(raw.response), this, obj.responderUsername, obj.responderAvatarIconUri && URI.revive(obj.responderAvatarIconUri), true, raw.isCanceled, raw.vote, raw.providerResponseId, raw.responseErrorDetails, raw.followups); } return request; }); @@ -308,7 +312,7 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS } const request = new InteractiveRequestModel(message, this._session.requesterUsername, this._session.requesterAvatarIconUri); - request.response = new InteractiveResponseModel(new MarkdownString(''), this._session.responderUsername, this.providerId, this._session.responderAvatarIconUri); + request.response = new InteractiveResponseModel(new MarkdownString(''), this, this._session.responderUsername, this._session.responderAvatarIconUri); this._requests.push(request); this._onDidChange.fire({ kind: 'addRequest', request }); @@ -321,7 +325,7 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS } if (!request.response) { - request.response = new InteractiveResponseModel(new MarkdownString(''), this._session.responderUsername, this.providerId, this._session.responderAvatarIconUri); + request.response = new InteractiveResponseModel(new MarkdownString(''), this, this._session.responderUsername, this._session.responderAvatarIconUri); } if (request.response.isComplete) { @@ -347,7 +351,7 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS } if (!request.response) { - request.response = new InteractiveResponseModel(new MarkdownString(''), this._session.responderUsername, this.providerId, this._session.responderAvatarIconUri); + request.response = new InteractiveResponseModel(new MarkdownString(''), this, this._session.responderUsername, this._session.responderAvatarIconUri); } request.response.complete(rawResponse.errorDetails); diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts index e3a009461f0b6..3f96a66adc265 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts @@ -27,6 +27,7 @@ export function isWelcomeVM(item: unknown): item is IInteractiveWelcomeMessageVi } export interface IInteractiveSessionViewModel { + readonly providerId: string; readonly sessionId: string; readonly onDidDisposeModel: Event; readonly onDidChange: Event; @@ -104,6 +105,10 @@ export class InteractiveSessionViewModel extends Disposable implements IInteract return this._model.requestInProgress; } + get providerId() { + return this._model.providerId; + } + constructor( private readonly _model: IInteractiveSessionModel, @IInstantiationService private readonly instantiationService: IInstantiationService, From 1b36c8d4fa4132c3656af80187ec5855cff790c2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 23 Apr 2023 22:13:59 -0700 Subject: [PATCH 2/2] Fix reloading editor after clear --- .../browser/interactiveSessionEditor.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts index a490db7c2abc8..261782866a4c7 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor.ts @@ -20,6 +20,7 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { InteractiveSessionEditorInput } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditorInput'; import { IViewState, InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget'; import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; +import { IInteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; export interface IInteractiveSessionEditorOptions extends IEditorOptions { providerId: string; @@ -61,12 +62,6 @@ export class InteractiveSessionEditor extends EditorPane { this.widget = this._register( scopedInstantiationService.createInstance(InteractiveSessionWidget, { resource: true }, () => editorBackground, () => SIDE_BAR_BACKGROUND, () => SIDE_BAR_BACKGROUND)); - this._register(this.widget.onDidChangeViewModel(() => { - // TODO replace with listening for model disposal - // This part is a bit odd. The widget's session and model will change. When that happens, store the latest session id - // on the EditorInput so that it can be restored when the editor moves or the window reloads. - (this.input! as InteractiveSessionEditorInput).sessionId = this.widget!.viewModel?.sessionId; - })); this.widget.render(parent); this.widget.setVisible(true); } @@ -94,9 +89,22 @@ export class InteractiveSessionEditor extends EditorPane { throw new Error('InteractiveSessionEditor lifecycle issue: no editor widget'); } - this._memento = new Memento('interactive-session-editor-' + editorModel.model.sessionId, this.storageService); + this.updateModel(editorModel.model, options); + } + + private updateModel(model: IInteractiveSessionModel, options: IInteractiveSessionEditorOptions): void { + this._memento = new Memento('interactive-session-editor-' + model.sessionId, this.storageService); this._viewState = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER) as IViewState; - this.widget.setModel(editorModel.model, { ...this._viewState }); + this.widget.setModel(model, { ...this._viewState }); + const listener = model.onDidDispose(() => { + // TODO go back to swapping out the EditorInput when the session is restarted instead of this listener + listener.dispose(); + const newModel = this.interactiveSessionService.startSession(options.providerId, false, CancellationToken.None); + if (newModel) { + (this.input as InteractiveSessionEditorInput).sessionId = newModel.sessionId; + this.updateModel(newModel, options); + } + }); } protected override saveState(): void {