inlineChat: shared history service with persistence (#303471)

This commit is contained in:
Johannes Rieken
2026-03-20 15:51:18 +01:00
committed by GitHub
parent f0a991b709
commit d05f2f2953
3 changed files with 112 additions and 14 deletions

View File

@@ -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 ---

View File

@@ -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>('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<string>;
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<string>(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();
}
}

View File

@@ -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<string>([''], 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 });