diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 37f26239b11..9f2dc09c60f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -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 { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.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'; @@ -145,6 +146,13 @@ export interface IWorkingSetEntry { uri: URI; } +const emptyInputState = observableMemento({ + defaultValue: undefined, + key: 'chat.untitledInputState', + toStorage: JSON.stringify, + fromStorage: JSON.parse, +}); + export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { private static _counter = 0; @@ -402,6 +410,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge */ private _generating?: { rc: number; defer: DeferredPromise }; + private _emptyInputState: ObservableMemento; + private _chatSessionIsEmpty = false; + constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used private readonly location: ChatAgentLocation, @@ -437,6 +448,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Initialize debounced text sync scheduler this._syncTextDebounced = this._register(new RunOnceScheduler(() => this._syncInputStateToModel(), 150)); + this._emptyInputState = this._register(emptyInputState(StorageScope.WORKSPACE, StorageTarget.USER, this.storageService)); this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); this._currentModeObservable = observableValue('currentMode', this.options.defaultMode ?? ChatMode.Agent); @@ -739,21 +751,55 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge /** * Set the input model reference for syncing input state */ - setInputModel(model: IInputModel | undefined): void { + setInputModel(model: IInputModel, chatSessionIsEmpty: boolean): void { this._inputModel = model; this._modelSyncDisposables.clear(); + this.selectedToolsModel.resetSessionEnablementState(); + this._chatSessionIsEmpty = chatSessionIsEmpty; - if (!model) { - return; + // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. + if (chatSessionIsEmpty) { + this._setEmptyModelState(); } // Observe changes from model and sync to view this._modelSyncDisposables.add(autorun(reader => { - const state = model.state.read(reader); + let state = model.state.read(reader); + if (!state && this._chatSessionIsEmpty) { + state = this._emptyInputState.read(undefined); + } + this._syncFromModel(state); })); } + private _setEmptyModelState() { + const storageKey = this.getDefaultModeExperimentStorageKey(); + const hasSetDefaultMode = this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false); + if (!hasSetDefaultMode) { + const isAnonymous = this.entitlementService.anonymous; + this.experimentService.getTreatment('chat.defaultMode') + .then((defaultModeTreatment => { + if (isAnonymous) { + // be deterministic for anonymous users + // to support agentic flows with default + // model. + defaultModeTreatment = ChatModeKind.Agent; + } + + if (typeof defaultModeTreatment === 'string') { + this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + const defaultMode = validateChatMode(defaultModeTreatment); + if (defaultMode) { + this.logService.trace(`Applying default mode from experiment: ${defaultMode}`); + this.setChatMode(defaultMode, false); + this.checkModelSupported(); + } + } + })); + } + } + /** * Sync from model to view (when model state changes) */ @@ -811,12 +857,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * Sync current input state to the input model */ private _syncInputStateToModel(): void { - if (!this._inputModel || this._isSyncingToOrFromInputModel) { + if (this._isSyncingToOrFromInputModel) { return; } + this._isSyncingToOrFromInputModel = true; - this._inputModel.setState(this.getCurrentInputState()); + const state = this.getCurrentInputState(); + if (this._chatSessionIsEmpty) { + this._emptyInputState.set(state, undefined); + } + this._inputModel?.setState(state); this._isSyncingToOrFromInputModel = false; } @@ -984,38 +1035,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - initForNewChatModel(state: IChatModelInputState | undefined, chatSessionIsEmpty: boolean): void { - this.selectedToolsModel.resetSessionEnablementState(); - - // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. - if (chatSessionIsEmpty) { - const storageKey = this.getDefaultModeExperimentStorageKey(); - const hasSetDefaultMode = this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false); - if (!hasSetDefaultMode) { - const isAnonymous = this.entitlementService.anonymous; - this.experimentService.getTreatment('chat.defaultMode') - .then((defaultModeTreatment => { - if (isAnonymous) { - // be deterministic for anonymous users - // to support agentic flows with default - // model. - defaultModeTreatment = ChatModeKind.Agent; - } - - if (typeof defaultModeTreatment === 'string') { - this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); - const defaultMode = validateChatMode(defaultModeTreatment); - if (defaultMode) { - this.logService.trace(`Applying default mode from experiment: ${defaultMode}`); - this.setChatMode(defaultMode, false); - this.checkModelSupported(); - } - } - })); - } - } - } - private getDefaultModeExperimentStorageKey(): string { const tag = this.options.widgetViewKindTag; return `chat.${tag}.hasSetDefaultModeByExperiment`; @@ -1161,6 +1180,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.history.append(this._getFilteredEntry(userQuery)); } + if (this._chatSessionIsEmpty) { + this._chatSessionIsEmpty = false; + this._emptyInputState.set(undefined, undefined); + } + // Clear attached context, fire event to clear input state, and clear the input editor this.attachmentModel.clear(); this._onDidLoadInputState.fire(); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 0ba701eacb3..d269895ad6c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1988,7 +1988,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection); // Pass input model reference to input part for state syncing - this.inputPart.setInputModel(model.inputModel); + this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0); if (this._lockedAgent) { let placeholder = this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgent.id); @@ -2034,8 +2034,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.viewModel = undefined; this.onDidChangeItems(); })); - const inputState = model.inputModel.state.get(); - this.input.initForNewChatModel(inputState, model.getRequests().length === 0); this._sessionIsEmptyContextKey.set(model.getRequests().length === 0); this.refreshParsedInput();