mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-08 15:55:15 +01:00
Scope chat input history per session
This commit is contained in:
@@ -97,6 +97,8 @@ Session-level properties are derived from chats:
|
||||
|
||||
The active session (`IActiveSession`) extends `ISession` with an `activeChat` observable that tracks which chat the user is viewing.
|
||||
|
||||
Chat input history in the Agents Window is scoped by `ISession.sessionId`. Pressing Up/Down in a chat input only navigates prompts previously submitted in the same session, including across multiple chats in that session. Users can disable `chat.agentSessions.scopedInputHistory` to restore shared input history across sessions. When a provider replaces a temporary untitled session with a committed session after the first send, history is moved from the temporary session id to the committed session id.
|
||||
|
||||
### Workspaces and Folders
|
||||
|
||||
Each session operates on an **`ISessionWorkspace`** containing one or more **`ISessionFolder`** instances. Folders encapsulate a working directory and optional git repository information (`ISessionGitRepository`), including branch state, upstream tracking, and GitHub PR info.
|
||||
|
||||
@@ -33,6 +33,7 @@ import { AccessibleViewRegistry } from '../../../../platform/accessibility/brows
|
||||
import { SessionsChatAccessibilityHelp } from './sessionsChatAccessibilityHelp.js';
|
||||
import { SessionsOpenerParticipantContribution } from './sessionsOpenerParticipant.js';
|
||||
import { WorktreeCreatedTaskDispatcher, AGENT_HOST_RUN_WORKTREE_CREATED_TASKS_SETTING } from './worktreeCreatedTaskDispatcher.js';
|
||||
import { AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING, SessionsChatHistoryContribution } from './sessionsChatHistoryContribution.js';
|
||||
import '../../sessions/browser/mobile/mobileOverlayContribution.js';
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import { EditorAreaFocusContext } from '../../../../workbench/common/contextkeys.js';
|
||||
@@ -78,6 +79,7 @@ registerAction2(BranchChatSessionAction);
|
||||
// register workbench contributions
|
||||
registerWorkbenchContribution2(RunScriptContribution.ID, RunScriptContribution, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(SessionsOpenerParticipantContribution.ID, SessionsOpenerParticipantContribution, WorkbenchPhase.BlockStartup);
|
||||
registerWorkbenchContribution2(SessionsChatHistoryContribution.ID, SessionsChatHistoryContribution, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(RegisterDefaultSessionTaskRunnersContribution.ID, RegisterDefaultSessionTaskRunnersContribution, WorkbenchPhase.BlockStartup);
|
||||
registerWorkbenchContribution2(WorktreeCreatedTaskDispatcher.ID, WorktreeCreatedTaskDispatcher, WorkbenchPhase.AfterRestored);
|
||||
|
||||
@@ -101,5 +103,11 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
description: localize('chat.agentHost.runWorktreeCreatedTasks', "Whether to automatically run tasks tagged with `\"runOptions\": { \"runOn\": \"worktreeCreated\" }` when a new agent host session worktree is created. Manual `Run Task` invocations are unaffected."),
|
||||
},
|
||||
[AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING]: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
description: localize('chat.agentSessions.scopedInputHistory', "Controls whether chat input history in the Agents Window is scoped to the current session. Disable this to use shared input history across sessions."),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ export class NewChatInSessionWidget extends Disposable {
|
||||
sendRequest: async ({ query, attachments }) => this._send(query, attachments),
|
||||
canSendRequest,
|
||||
loading,
|
||||
historyKey: derived(reader => this.sessionsManagementService.activeSession.read(reader)?.sessionId),
|
||||
minEditorHeight: 64,
|
||||
placeholder: localize('newChatInSessionPlaceholder', 'Ask a follow-up question or start a new topic within this session...'),
|
||||
}));
|
||||
|
||||
@@ -56,6 +56,7 @@ import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext }
|
||||
import { autorun, IObservable } from '../../../../base/common/observable.js';
|
||||
import { ChatInputNotificationWidget } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputNotificationWidget.js';
|
||||
import { INewChatModelPickerService, NewChatModelPickerService } from './newChatModelPicker.js';
|
||||
import { AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING } from './sessionsChatHistoryContribution.js';
|
||||
|
||||
|
||||
const STORAGE_KEY_DRAFT_STATE = 'sessions.draftState';
|
||||
@@ -159,6 +160,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
sendRequest: (request: INewChatInputSendRequest) => Promise<void>;
|
||||
canSendRequest: IObservable<boolean>;
|
||||
loading: IObservable<boolean>;
|
||||
historyKey?: IObservable<string | undefined>;
|
||||
minEditorHeight?: number;
|
||||
placeholder?: string;
|
||||
renderSessionTypePickerInControls?: boolean;
|
||||
@@ -179,6 +181,14 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
[INewChatModelPickerService, new NewChatModelPickerService()],
|
||||
)));
|
||||
this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat));
|
||||
if (this.options.historyKey) {
|
||||
this._register(autorun(reader => this._setHistoryKey(this.options.historyKey?.read(reader))));
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING)) {
|
||||
this._setHistoryKey(this.options.historyKey?.get());
|
||||
}
|
||||
}));
|
||||
}
|
||||
this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments));
|
||||
// Always use the mobile-aware picker. Its overrides bail to the
|
||||
// desktop behavior when `isPhoneLayout()` is false, so picking
|
||||
@@ -199,6 +209,10 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
}));
|
||||
}
|
||||
|
||||
private _setHistoryKey(historyKey: string | undefined): void {
|
||||
this._history.setHistoryKey(this.configurationService.getValue<boolean>(AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING) !== false ? historyKey : undefined);
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
|
||||
render(parent: HTMLElement, root: HTMLElement): void {
|
||||
|
||||
@@ -77,6 +77,7 @@ export class NewChatWidget extends Disposable {
|
||||
sendRequest: async ({ query, attachments, background }) => this._send(query, attachments, background),
|
||||
canSendRequest,
|
||||
loading,
|
||||
historyKey: derived(reader => this.sessionsManagementService.activeSession.read(reader)?.sessionId),
|
||||
renderSessionTypePickerInControls: false,
|
||||
supportsBackground: true,
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
|
||||
import { IChatWidget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
|
||||
import { ChatAgentLocation } from '../../../../workbench/contrib/chat/common/constants.js';
|
||||
import { IChatWidgetHistoryService } from '../../../../workbench/contrib/chat/common/widget/chatWidgetHistoryService.js';
|
||||
import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';
|
||||
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
|
||||
import { ISession } from '../../../services/sessions/common/session.js';
|
||||
|
||||
export const AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING = 'chat.agentSessions.scopedInputHistory';
|
||||
|
||||
export class SessionsChatHistoryContribution extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
static readonly ID = 'sessions.chatHistory';
|
||||
|
||||
private readonly _registeredWidgets = new Set<IChatWidget>();
|
||||
private readonly _widgetDisposables = this._register(new DisposableStore());
|
||||
|
||||
constructor(
|
||||
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
|
||||
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
|
||||
@IChatWidgetHistoryService private readonly chatWidgetHistoryService: IChatWidgetHistoryService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
for (const widget of this.chatWidgetService.getAllWidgets()) {
|
||||
this._registerWidget(widget);
|
||||
}
|
||||
|
||||
this._register(this.chatWidgetService.onDidAddWidget(widget => this._registerWidget(widget)));
|
||||
this._register(this.sessionsManagementService.onDidChangeSessions(() => this._applyHistoryKeys()));
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING)) {
|
||||
this._applyHistoryKeys();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this.sessionsManagementService.onDidReplaceSession(e => {
|
||||
this.chatWidgetHistoryService.moveHistory(ChatAgentLocation.Chat, e.from.sessionId, e.to.sessionId);
|
||||
this._applyHistoryKeys();
|
||||
}));
|
||||
|
||||
this._register(autorun(reader => {
|
||||
this.sessionsManagementService.activeSession.read(reader)?.activeChat.read(reader);
|
||||
this._applyHistoryKeys();
|
||||
}));
|
||||
}
|
||||
|
||||
private _registerWidget(widget: IChatWidget): void {
|
||||
if (this._registeredWidgets.has(widget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._registeredWidgets.add(widget);
|
||||
this._widgetDisposables.add(widget.onDidChangeViewModel(() => this._applyHistoryKey(widget)));
|
||||
this._applyHistoryKey(widget);
|
||||
}
|
||||
|
||||
private _applyHistoryKeys(): void {
|
||||
for (const widget of this.chatWidgetService.getAllWidgets()) {
|
||||
this._applyHistoryKey(widget);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyHistoryKey(widget: IChatWidget): void {
|
||||
const sessionResource = widget.viewModel?.sessionResource;
|
||||
widget.inputPart.setHistoryKey(this._useScopedInputHistory() && sessionResource ? this._getHistoryKey(sessionResource) : undefined);
|
||||
}
|
||||
|
||||
private _useScopedInputHistory(): boolean {
|
||||
return this.configurationService.getValue<boolean>(AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING) !== false;
|
||||
}
|
||||
|
||||
private _getHistoryKey(sessionResource: URI): string | undefined {
|
||||
const activeSession = this.sessionsManagementService.activeSession.get();
|
||||
if (activeSession && this._matchesSession(activeSession, sessionResource)) {
|
||||
return activeSession.sessionId;
|
||||
}
|
||||
|
||||
for (const session of this.sessionsManagementService.getSessions()) {
|
||||
if (this._matchesSession(session, sessionResource)) {
|
||||
return session.sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _matchesSession(session: ISession, sessionResource: URI): boolean {
|
||||
if (this.uriIdentityService.extUri.isEqual(session.resource, sessionResource)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return session.chats.get().some(chat => this.uriIdentityService.extUri.isEqual(chat.resource, sessionResource));
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,9 @@ export class SessionsManagementService extends Disposable implements ISessionsMa
|
||||
private readonly _onDidToggleSessionStickiness = this._register(new Emitter<IToggleSessionStickinessEvent>());
|
||||
readonly onDidToggleSessionStickiness: Event<IToggleSessionStickinessEvent> = this._onDidToggleSessionStickiness.event;
|
||||
|
||||
private readonly _onDidReplaceSession = this._register(new Emitter<{ readonly from: ISession; readonly to: ISession }>());
|
||||
readonly onDidReplaceSession: Event<{ readonly from: ISession; readonly to: ISession }> = this._onDidReplaceSession.event;
|
||||
|
||||
private readonly _onDidChangeSessionTypes = this._register(new Emitter<void>());
|
||||
readonly onDidChangeSessionTypes: Event<void> = this._onDidChangeSessionTypes.event;
|
||||
|
||||
@@ -294,7 +297,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa
|
||||
const disposables = new DisposableStore();
|
||||
disposables.add(provider.onDidChangeSessions(e => this.onDidChangeSessionsFromSessionsProviders(e)));
|
||||
if (provider.onDidReplaceSession) {
|
||||
disposables.add(provider.onDidReplaceSession(e => this.onDidReplaceSession(e.from, e.to)));
|
||||
disposables.add(provider.onDidReplaceSession(e => this._handleDidReplaceSession(e.from, e.to)));
|
||||
}
|
||||
if (provider.onDidChangeSessionTypes) {
|
||||
disposables.add(provider.onDidChangeSessionTypes(() => this._updateSessionTypes()));
|
||||
@@ -303,9 +306,9 @@ export class SessionsManagementService extends Disposable implements ISessionsMa
|
||||
}
|
||||
}
|
||||
|
||||
private onDidReplaceSession(from: ISession, to: ISession): void {
|
||||
private _handleDidReplaceSession(from: ISession, to: ISession): void {
|
||||
this._visibility.updateSession(from, to);
|
||||
|
||||
this._onDidReplaceSession.fire({ from, to });
|
||||
// Always fire the change event so the SessionsList refreshes even when
|
||||
// the user navigated to a different session while the new one was
|
||||
// being created (which is how duplicate rows appeared in the list).
|
||||
|
||||
@@ -181,6 +181,9 @@ export interface ISessionsManagementService {
|
||||
/** Fires after a session's stickiness was toggled via {@link toggleSessionStickiness}. */
|
||||
readonly onDidToggleSessionStickiness: Event<IToggleSessionStickinessEvent>;
|
||||
|
||||
/** Fires when a provider replaces a temporary session with its committed session. */
|
||||
readonly onDidReplaceSession: Event<{ readonly from: ISession; readonly to: ISession }>;
|
||||
|
||||
// -- Active Session --
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,6 +96,7 @@ class MockSessionStore implements ISessionsManagementService {
|
||||
readonly onDidDeleteChat = Event.None;
|
||||
readonly onDidRenameChat = Event.None;
|
||||
readonly onDidToggleSessionStickiness = Event.None;
|
||||
readonly onDidReplaceSession = Event.None;
|
||||
|
||||
private readonly _sessions = new Map<string, ISession>();
|
||||
private _openedResource: URI | undefined;
|
||||
|
||||
@@ -378,6 +378,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
return this._inputEditor;
|
||||
}
|
||||
|
||||
setHistoryKey(historyKey: string | undefined): void {
|
||||
this.history.setHistoryKey(historyKey);
|
||||
}
|
||||
|
||||
readonly dnd: ChatDragAndDrop;
|
||||
|
||||
private history: ChatHistoryNavigator;
|
||||
|
||||
@@ -39,15 +39,16 @@ export interface IChatWidgetHistoryService {
|
||||
readonly onDidChangeHistory: Event<ChatHistoryChange>;
|
||||
|
||||
clearHistory(): void;
|
||||
getHistory(location: ChatAgentLocation): readonly IChatModelInputState[];
|
||||
append(location: ChatAgentLocation, history: IChatModelInputState): void;
|
||||
getHistory(location: ChatAgentLocation, historyKey?: string): readonly IChatModelInputState[];
|
||||
append(location: ChatAgentLocation, history: IChatModelInputState, historyKey?: string): void;
|
||||
moveHistory(location: ChatAgentLocation, fromHistoryKey: string, toHistoryKey: string): void;
|
||||
}
|
||||
|
||||
interface IChatHistory {
|
||||
history?: { [providerId: string]: IChatModelInputState[] };
|
||||
}
|
||||
|
||||
export type ChatHistoryChange = { kind: 'append'; entry: IChatModelInputState } | { kind: 'clear' };
|
||||
export type ChatHistoryChange = { kind: 'append'; location: ChatAgentLocation; historyKey: string | undefined; entry: IChatModelInputState } | { kind: 'move'; location: ChatAgentLocation; fromHistoryKey: string; toHistoryKey: string } | { kind: 'clear' };
|
||||
|
||||
export const ChatInputHistoryMaxEntries = 40;
|
||||
|
||||
@@ -78,8 +79,8 @@ export class ChatWidgetHistoryService extends Disposable implements IChatWidgetH
|
||||
}));
|
||||
}
|
||||
|
||||
getHistory(location: ChatAgentLocation): IChatModelInputState[] {
|
||||
const key = this.getKey(location);
|
||||
getHistory(location: ChatAgentLocation, historyKey?: string): IChatModelInputState[] {
|
||||
const key = this.getKey(location, historyKey);
|
||||
const history = this.viewState.history?.[key] ?? [];
|
||||
return history.map(entry => this.migrateHistoryEntry(entry));
|
||||
}
|
||||
@@ -133,18 +134,39 @@ export class ChatWidgetHistoryService extends Disposable implements IChatWidgetH
|
||||
};
|
||||
}
|
||||
|
||||
private getKey(location: ChatAgentLocation): string {
|
||||
private getKey(location: ChatAgentLocation, historyKey?: string): string {
|
||||
// Preserve history for panel by continuing to use the same old provider id. Use the location as a key for other chat locations.
|
||||
return location === ChatAgentLocation.Chat ? CHAT_PROVIDER_ID : location;
|
||||
const locationKey = location === ChatAgentLocation.Chat ? CHAT_PROVIDER_ID : location;
|
||||
return historyKey === undefined ? locationKey : `${locationKey}:${historyKey}`;
|
||||
}
|
||||
|
||||
append(location: ChatAgentLocation, history: IChatModelInputState): void {
|
||||
append(location: ChatAgentLocation, history: IChatModelInputState, historyKey?: string): void {
|
||||
this.viewState.history ??= {};
|
||||
|
||||
const key = this.getKey(location);
|
||||
this.viewState.history[key] = this.getHistory(location).concat(history).slice(-ChatInputHistoryMaxEntries);
|
||||
const key = this.getKey(location, historyKey);
|
||||
this.viewState.history[key] = this.getHistory(location, historyKey).concat(history).slice(-ChatInputHistoryMaxEntries);
|
||||
this.changed = true;
|
||||
this._onDidChangeHistory.fire({ kind: 'append', entry: history });
|
||||
this._onDidChangeHistory.fire({ kind: 'append', location, historyKey, entry: history });
|
||||
}
|
||||
|
||||
moveHistory(location: ChatAgentLocation, fromHistoryKey: string, toHistoryKey: string): void {
|
||||
if (fromHistoryKey === toHistoryKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromHistory = this.getHistory(location, fromHistoryKey);
|
||||
if (fromHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewState.history ??= {};
|
||||
|
||||
const fromKey = this.getKey(location, fromHistoryKey);
|
||||
const toKey = this.getKey(location, toHistoryKey);
|
||||
this.viewState.history[toKey] = this.getHistory(location, toHistoryKey).concat(fromHistory).slice(-ChatInputHistoryMaxEntries);
|
||||
delete this.viewState.history[fromKey];
|
||||
this.changed = true;
|
||||
this._onDidChangeHistory.fire({ kind: 'move', location, fromHistoryKey, toHistoryKey });
|
||||
}
|
||||
|
||||
clearHistory(): void {
|
||||
@@ -161,9 +183,10 @@ export class ChatHistoryNavigator extends Disposable {
|
||||
private _currentIndex: number;
|
||||
private _history: readonly IChatModelInputState[];
|
||||
private _overlay: (IChatModelInputState | undefined)[] = [];
|
||||
private _historyKey: string | undefined;
|
||||
|
||||
public get values() {
|
||||
return this.chatWidgetHistoryService.getHistory(this.location);
|
||||
return this.chatWidgetHistoryService.getHistory(this.location, this._historyKey);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -171,13 +194,16 @@ export class ChatHistoryNavigator extends Disposable {
|
||||
@IChatWidgetHistoryService private readonly chatWidgetHistoryService: IChatWidgetHistoryService
|
||||
) {
|
||||
super();
|
||||
this._history = this.chatWidgetHistoryService.getHistory(this.location);
|
||||
this._history = this.chatWidgetHistoryService.getHistory(this.location, this._historyKey);
|
||||
this._currentIndex = this._history.length;
|
||||
|
||||
this._register(this.chatWidgetHistoryService.onDidChangeHistory(e => {
|
||||
if (e.kind === 'append') {
|
||||
if (e.location !== this.location || e.historyKey !== this._historyKey) {
|
||||
return;
|
||||
}
|
||||
const prevLength = this._history.length;
|
||||
this._history = this.chatWidgetHistoryService.getHistory(this.location);
|
||||
this._history = this.chatWidgetHistoryService.getHistory(this.location, this._historyKey);
|
||||
const newLength = this._history.length;
|
||||
|
||||
// If this append operation adjusted all history entries back, move our index back too
|
||||
@@ -194,10 +220,28 @@ export class ChatHistoryNavigator extends Disposable {
|
||||
this._history = [];
|
||||
this._currentIndex = 0;
|
||||
this._overlay = [];
|
||||
} else if (e.kind === 'move') {
|
||||
if (e.location !== this.location || (e.fromHistoryKey !== this._historyKey && e.toHistoryKey !== this._historyKey)) {
|
||||
return;
|
||||
}
|
||||
this._history = this.chatWidgetHistoryService.getHistory(this.location, this._historyKey);
|
||||
this._currentIndex = this._history.length;
|
||||
this._overlay = [];
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public setHistoryKey(historyKey: string | undefined) {
|
||||
if (this._historyKey === historyKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._historyKey = historyKey;
|
||||
this._history = this.chatWidgetHistoryService.getHistory(this.location, this._historyKey);
|
||||
this._currentIndex = this._history.length;
|
||||
this._overlay = [];
|
||||
}
|
||||
|
||||
public isAtEnd() {
|
||||
return this._currentIndex === Math.max(this._history.length, this._overlay.length);
|
||||
}
|
||||
@@ -242,7 +286,7 @@ export class ChatHistoryNavigator extends Disposable {
|
||||
this._currentIndex = this._history.length;
|
||||
|
||||
if (!entriesEqual(this._history.at(-1), entry)) {
|
||||
this.chatWidgetHistoryService.append(this.location, entry);
|
||||
this.chatWidgetHistoryService.append(this.location, entry, this._historyKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,39 @@ suite('ChatWidgetHistoryService', () => {
|
||||
assert.strictEqual(terminalHistory[0].inputText, 'terminal query');
|
||||
});
|
||||
|
||||
test('should maintain separate history per history key', () => {
|
||||
const historyService = createHistoryService();
|
||||
historyService.append(ChatAgentLocation.Chat, createInputState('global query'));
|
||||
historyService.append(ChatAgentLocation.Chat, createInputState('session 1 query'), 'session-1');
|
||||
historyService.append(ChatAgentLocation.Chat, createInputState('session 2 query'), 'session-2');
|
||||
|
||||
assert.deepStrictEqual({
|
||||
global: historyService.getHistory(ChatAgentLocation.Chat).map(entry => entry.inputText),
|
||||
session1: historyService.getHistory(ChatAgentLocation.Chat, 'session-1').map(entry => entry.inputText),
|
||||
session2: historyService.getHistory(ChatAgentLocation.Chat, 'session-2').map(entry => entry.inputText),
|
||||
}, {
|
||||
global: ['global query'],
|
||||
session1: ['session 1 query'],
|
||||
session2: ['session 2 query'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should move history between history keys', () => {
|
||||
const historyService = createHistoryService();
|
||||
historyService.append(ChatAgentLocation.Chat, createInputState('committed query'), 'committed-session');
|
||||
historyService.append(ChatAgentLocation.Chat, createInputState('untitled query'), 'untitled-session');
|
||||
|
||||
historyService.moveHistory(ChatAgentLocation.Chat, 'untitled-session', 'committed-session');
|
||||
|
||||
assert.deepStrictEqual({
|
||||
untitled: historyService.getHistory(ChatAgentLocation.Chat, 'untitled-session').map(entry => entry.inputText),
|
||||
committed: historyService.getHistory(ChatAgentLocation.Chat, 'committed-session').map(entry => entry.inputText),
|
||||
}, {
|
||||
untitled: [],
|
||||
committed: ['committed query', 'untitled query'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should limit history to max entries', () => {
|
||||
const historyService = createHistoryService();
|
||||
for (let i = 0; i < ChatInputHistoryMaxEntries + 10; i++) {
|
||||
@@ -410,4 +443,52 @@ suite('ChatHistoryNavigator', () => {
|
||||
assert.strictEqual(nav2.current()?.inputText, 'query 1');
|
||||
assert.strictEqual(nav2.values.length, 4);
|
||||
});
|
||||
|
||||
test('should keep concurrent navigators separated by history key', () => {
|
||||
const instantiationService = testDisposables.add(new TestInstantiationService());
|
||||
const storageService = testDisposables.add(new TestStorageService());
|
||||
instantiationService.stub(IStorageService, storageService);
|
||||
|
||||
const historyService = testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService));
|
||||
instantiationService.stub(IChatWidgetHistoryService, historyService);
|
||||
|
||||
const nav1 = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat));
|
||||
const nav2 = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat));
|
||||
nav1.setHistoryKey('session-1');
|
||||
nav2.setHistoryKey('session-2');
|
||||
|
||||
nav1.append(createInputState('session 1 query 1'));
|
||||
nav1.append(createInputState('session 1 query 2'));
|
||||
nav2.append(createInputState('session 2 query'));
|
||||
|
||||
nav1.previous();
|
||||
nav2.append(createInputState('session 2 query 2'));
|
||||
|
||||
assert.deepStrictEqual({
|
||||
nav1Current: nav1.current()?.inputText,
|
||||
nav1Values: nav1.values.map(entry => entry.inputText),
|
||||
nav2Values: nav2.values.map(entry => entry.inputText),
|
||||
}, {
|
||||
nav1Current: 'session 1 query 2',
|
||||
nav1Values: ['session 1 query 1', 'session 1 query 2'],
|
||||
nav2Values: ['session 2 query', 'session 2 query 2'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should update navigator when scoped history moves', () => {
|
||||
const instantiationService = testDisposables.add(new TestInstantiationService());
|
||||
const storageService = testDisposables.add(new TestStorageService());
|
||||
instantiationService.stub(IStorageService, storageService);
|
||||
|
||||
const historyService = testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService));
|
||||
instantiationService.stub(IChatWidgetHistoryService, historyService);
|
||||
|
||||
const nav = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat));
|
||||
nav.setHistoryKey('committed-session');
|
||||
|
||||
historyService.append(ChatAgentLocation.Chat, createInputState('untitled query'), 'untitled-session');
|
||||
historyService.moveHistory(ChatAgentLocation.Chat, 'untitled-session', 'committed-session');
|
||||
|
||||
assert.deepStrictEqual(nav.values.map(entry => entry.inputText), ['untitled query']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user