From d05f2f29537c6e7a840fa64e38ec5ba6ea99ba85 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 20 Mar 2026 15:51:18 +0100 Subject: [PATCH] inlineChat: shared history service with persistence (#303471) --- .../browser/inlineChat.contribution.ts | 2 + .../browser/inlineChatHistoryService.ts | 94 +++++++++++++++++++ .../browser/inlineChatOverlayWidget.ts | 30 +++--- 3 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index e5ad9450dc1..acb788bced6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -17,6 +17,7 @@ import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatEscapeToolContribution, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; +import { IInlineChatHistoryService, InlineChatHistoryService } from './inlineChatHistoryService.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { CancelAction, ChatSubmitAction } from '../../chat/browser/actions/chatExecuteActions.js'; import { localize } from '../../../../nls.js'; @@ -36,6 +37,7 @@ registerAction2(InlineChatActions.RephraseInlineChatSessionAction); // --- browser registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); +registerSingleton(IInlineChatHistoryService, InlineChatHistoryService, InstantiationType.Delayed); // --- MENU special --- diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts new file mode 100644 index 00000000000..3b501b8a03e --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { HistoryNavigator2 } from '../../../../base/common/history.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +export const IInlineChatHistoryService = createDecorator('IInlineChatHistoryService'); + +export interface IInlineChatHistoryService { + readonly _serviceBrand: undefined; + + addToHistory(value: string): void; + previousValue(): string | undefined; + nextValue(): string | undefined; + isAtEnd(): boolean; + replaceLast(value: string): void; + resetCursor(): void; +} + +const _storageKey = 'inlineChat.history'; +const _capacity = 50; + +export class InlineChatHistoryService extends Disposable implements IInlineChatHistoryService { + declare readonly _serviceBrand: undefined; + + private readonly _history: HistoryNavigator2; + + constructor( + @IStorageService private readonly _storageService: IStorageService, + ) { + super(); + + const raw = this._storageService.get(_storageKey, StorageScope.PROFILE); + let entries: string[] = ['']; + if (raw) { + try { + const parsed: string[] = JSON.parse(raw); + if (Array.isArray(parsed) && parsed.length > 0) { + entries = parsed; + // Ensure there's always an empty uncommitted entry at the end + if (entries[entries.length - 1] !== '') { + entries.push(''); + } + } + } catch { + // ignore invalid data + } + } + + this._history = new HistoryNavigator2(entries, _capacity); + + this._store.add(this._storageService.onWillSaveState(() => { + this._saveToStorage(); + })); + } + + private _saveToStorage(): void { + const values = [...this._history].filter(v => v.length > 0); + if (values.length === 0) { + this._storageService.remove(_storageKey, StorageScope.PROFILE); + } else { + this._storageService.store(_storageKey, JSON.stringify(values), StorageScope.PROFILE, StorageTarget.USER); + } + } + + addToHistory(value: string): void { + this._history.replaceLast(value); + this._history.add(''); + } + + previousValue(): string | undefined { + return this._history.previous(); + } + + nextValue(): string | undefined { + return this._history.next(); + } + + isAtEnd(): boolean { + return this._history.isAtEnd(); + } + + replaceLast(value: string): void { + this._history.replaceLast(value); + } + + resetCursor(): void { + this._history.resetCursor(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 2d0b0d6f810..3675acc488e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -12,7 +12,6 @@ import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actio import { BaseActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { HistoryNavigator2 } from '../../../../base/common/history.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -42,6 +41,7 @@ import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOpt import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; import { IInlineChatSession2 } from './inlineChatSessionService.js'; import { assertType } from '../../../../base/common/types.js'; +import { IInlineChatHistoryService } from './inlineChatHistoryService.js'; /** * Overlay widget that displays a vertical action bar menu. @@ -63,8 +63,6 @@ export class InlineChatInputWidget extends Disposable { private _anchorLeft: number = 0; private _anchorAbove: boolean = false; - private readonly _historyNavigator = new HistoryNavigator2([''], 50); - constructor( private readonly _editorObs: ObservableCodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -72,6 +70,7 @@ export class InlineChatInputWidget extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @IConfigurationService configurationService: IConfigurationService, + @IInlineChatHistoryService private readonly _historyService: IInlineChatHistoryService, ) { super(); @@ -237,7 +236,7 @@ export class InlineChatInputWidget extends Disposable { const model = this._input.getModel(); const position = this._input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { - if (!this._historyNavigator.isAtEnd()) { + if (!this._historyService.isAtEnd()) { this._showNextHistoryValue(); e.preventDefault(); e.stopPropagation(); @@ -278,24 +277,27 @@ export class InlineChatInputWidget extends Disposable { } addToHistory(value: string): void { - this._historyNavigator.replaceLast(value); - this._historyNavigator.add(''); + this._historyService.addToHistory(value); } private _showPreviousHistoryValue(): void { - if (this._historyNavigator.isAtEnd()) { - this._historyNavigator.replaceLast(this._input.getModel().getValue()); + if (this._historyService.isAtEnd()) { + this._historyService.replaceLast(this._input.getModel().getValue()); + } + const value = this._historyService.previousValue(); + if (value !== undefined) { + this._input.getModel().setValue(value); } - const value = this._historyNavigator.previous(); - this._input.getModel().setValue(value); } private _showNextHistoryValue(): void { - if (this._historyNavigator.isAtEnd()) { + if (this._historyService.isAtEnd()) { return; } - const value = this._historyNavigator.next(); - this._input.getModel().setValue(value); + const value = this._historyService.nextValue(); + if (value !== undefined) { + this._input.getModel().setValue(value); + } } /** @@ -308,7 +310,7 @@ export class InlineChatInputWidget extends Disposable { this._showStore.clear(); // Reset history cursor to the end (current uncommitted text) - this._historyNavigator.resetCursor(); + this._historyService.resetCursor(); // Clear input state this._input.updateOptions({ wordWrap: 'off', placeholder });