mirror of
https://github.com/Microsoft/vscode
synced 2024-10-05 19:02:54 +00:00
Improve chat input history (#215593)
* Fixes to history navigation with dynamic variables Prevent ChatDynamicVariableModel deleting editor text while in history navigation * Try to add history item for current input, doesn't quite work * Use HistoryNavigator2 in chat * Update context attachments for new input state model * Add SetWithKey and use it in HistoryNavigator2 * Fix history for 2 widget inline chat * Add log action, clean up * Add tests for SetWithKey * comment * Fix
This commit is contained in:
parent
be838c0af7
commit
35ac2d01a6
|
@ -80,3 +80,61 @@ export function intersection<T>(setA: Set<T>, setB: Iterable<T>): Set<T> {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class SetWithKey<T> implements Set<T> {
|
||||
private _map = new Map<any, T>();
|
||||
|
||||
constructor(values: T[], private toKey: (t: T) => any) {
|
||||
for (const value of values) {
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._map.size;
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
const key = this.toKey(value);
|
||||
this._map.set(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
return this._map.delete(this.toKey(value));
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this._map.has(this.toKey(value));
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[T, T]> {
|
||||
for (const entry of this._map.values()) {
|
||||
yield [entry, entry];
|
||||
}
|
||||
}
|
||||
|
||||
keys(): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
*values(): IterableIterator<T> {
|
||||
for (const entry of this._map.values()) {
|
||||
yield entry;
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._map.clear();
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this));
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
[Symbol.toStringTag]: string = 'SetWithKey';
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SetWithKey } from 'vs/base/common/collections';
|
||||
import { ArrayNavigator, INavigator } from 'vs/base/common/navigator';
|
||||
|
||||
export class HistoryNavigator<T> implements INavigator<T> {
|
||||
|
@ -114,6 +115,10 @@ interface HistoryNode<T> {
|
|||
next: HistoryNode<T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The right way to use HistoryNavigator2 is for the last item in the list to be the user's uncommitted current text. eg empty string, or whatever has been typed. Then
|
||||
* the user can navigate away from the last item through the list, and back to it. When updating the last item, call replaceLast.
|
||||
*/
|
||||
export class HistoryNavigator2<T> {
|
||||
|
||||
private valueSet: Set<T>;
|
||||
|
@ -123,7 +128,7 @@ export class HistoryNavigator2<T> {
|
|||
private _size: number;
|
||||
get size(): number { return this._size; }
|
||||
|
||||
constructor(history: readonly T[], private capacity: number = 10) {
|
||||
constructor(history: readonly T[], private capacity: number = 10, private identityFn: (t: T) => any = t => t) {
|
||||
if (history.length < 1) {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
@ -135,7 +140,7 @@ export class HistoryNavigator2<T> {
|
|||
next: undefined
|
||||
};
|
||||
|
||||
this.valueSet = new Set<T>([history[0]]);
|
||||
this.valueSet = new SetWithKey<T>([history[0]], identityFn);
|
||||
for (let i = 1; i < history.length; i++) {
|
||||
this.add(history[i]);
|
||||
}
|
||||
|
@ -172,7 +177,7 @@ export class HistoryNavigator2<T> {
|
|||
* @returns old last value
|
||||
*/
|
||||
replaceLast(value: T): T {
|
||||
if (this.tail.value === value) {
|
||||
if (this.identityFn(this.tail.value) === this.identityFn(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -252,8 +257,9 @@ export class HistoryNavigator2<T> {
|
|||
private _deleteFromList(value: T): void {
|
||||
let temp = this.head;
|
||||
|
||||
const valueKey = this.identityFn(value);
|
||||
while (temp !== this.tail) {
|
||||
if (temp.value === value) {
|
||||
if (this.identityFn(temp.value) === valueKey) {
|
||||
if (temp === this.head) {
|
||||
this.head = this.head.next!;
|
||||
this.head.previous = undefined;
|
||||
|
|
|
@ -32,4 +32,70 @@ suite('Collections', () => {
|
|||
assert.strictEqual(grouped[group2].length, 1);
|
||||
assert.strictEqual(grouped[group2][0].value, value3);
|
||||
});
|
||||
|
||||
suite('SetWithKey', () => {
|
||||
let setWithKey: collections.SetWithKey<{ someProp: string }>;
|
||||
|
||||
const initialValues = ['a', 'b', 'c'].map(s => ({ someProp: s }));
|
||||
setup(() => {
|
||||
setWithKey = new collections.SetWithKey<{ someProp: string }>(initialValues, value => value.someProp);
|
||||
});
|
||||
|
||||
test('size', () => {
|
||||
assert.strictEqual(setWithKey.size, 3);
|
||||
});
|
||||
|
||||
test('add', () => {
|
||||
setWithKey.add({ someProp: 'd' });
|
||||
assert.strictEqual(setWithKey.size, 4);
|
||||
assert.strictEqual(setWithKey.has({ someProp: 'd' }), true);
|
||||
});
|
||||
|
||||
test('delete', () => {
|
||||
assert.strictEqual(setWithKey.has({ someProp: 'b' }), true);
|
||||
setWithKey.delete({ someProp: 'b' });
|
||||
assert.strictEqual(setWithKey.size, 2);
|
||||
assert.strictEqual(setWithKey.has({ someProp: 'b' }), false);
|
||||
});
|
||||
|
||||
test('has', () => {
|
||||
assert.strictEqual(setWithKey.has({ someProp: 'a' }), true);
|
||||
assert.strictEqual(setWithKey.has({ someProp: 'b' }), true);
|
||||
});
|
||||
|
||||
test('entries', () => {
|
||||
const entries = Array.from(setWithKey.entries());
|
||||
assert.deepStrictEqual(entries, initialValues.map(value => [value, value]));
|
||||
});
|
||||
|
||||
test('keys and values', () => {
|
||||
const keys = Array.from(setWithKey.keys());
|
||||
const values = Array.from(setWithKey.values());
|
||||
assert.deepStrictEqual(keys, initialValues);
|
||||
assert.deepStrictEqual(values, initialValues);
|
||||
});
|
||||
|
||||
test('clear', () => {
|
||||
setWithKey.clear();
|
||||
assert.strictEqual(setWithKey.size, 0);
|
||||
});
|
||||
|
||||
test('forEach', () => {
|
||||
const values: any[] = [];
|
||||
setWithKey.forEach(value => values.push(value));
|
||||
assert.deepStrictEqual(values, initialValues);
|
||||
});
|
||||
|
||||
test('iterator', () => {
|
||||
const values: any[] = [];
|
||||
for (const value of setWithKey) {
|
||||
values.push(value);
|
||||
}
|
||||
assert.deepStrictEqual(values, initialValues);
|
||||
});
|
||||
|
||||
test('toStringTag', () => {
|
||||
assert.strictEqual(setWithKey[Symbol.toStringTag], 'SetWithKey');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { localize2 } from 'vs/nls';
|
||||
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
|
||||
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
|
||||
|
||||
export function registerChatDeveloperActions() {
|
||||
registerAction2(LogChatInputHistoryAction);
|
||||
}
|
||||
|
||||
class LogChatInputHistoryAction extends Action2 {
|
||||
|
||||
static readonly ID = 'workbench.action.chat.logInputHistory';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: LogChatInputHistoryAction.ID,
|
||||
title: localize2('workbench.action.chat.logInputHistory.label', "Log Chat Input History"),
|
||||
icon: Codicon.attach,
|
||||
category: Categories.Developer,
|
||||
f1: true
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
|
||||
const chatWidgetService = accessor.get(IChatWidgetService);
|
||||
chatWidgetService.lastFocusedWidget?.logInputHistory();
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/s
|
|||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import '../common/chatColors';
|
||||
import { registerChatContextActions } from 'vs/workbench/contrib/chat/browser/actions/chatContextActions';
|
||||
import { registerChatDeveloperActions } from 'vs/workbench/contrib/chat/browser/actions/chatDeveloperActions';
|
||||
|
||||
// Register configuration
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
|
@ -250,6 +251,7 @@ registerChatExportActions();
|
|||
registerMoveActions();
|
||||
registerNewChatActions();
|
||||
registerChatContextActions();
|
||||
registerChatDeveloperActions();
|
||||
|
||||
registerSingleton(IChatService, ChatService, InstantiationType.Delayed);
|
||||
registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed);
|
||||
|
|
|
@ -157,6 +157,7 @@ export interface IChatWidget {
|
|||
getFocus(): ChatTreeItem | undefined;
|
||||
setInput(query?: string): void;
|
||||
getInput(): string;
|
||||
logInputHistory(): void;
|
||||
acceptInput(query?: string): Promise<IChatResponseModel | undefined>;
|
||||
acceptInputWithPrefix(prefix: string): void;
|
||||
setInputPlaceholder(placeholder: string): void;
|
||||
|
|
|
@ -6,14 +6,16 @@
|
|||
import * as dom from 'vs/base/browser/dom';
|
||||
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
|
||||
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import * as aria from 'vs/base/browser/ui/aria/aria';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Button } from 'vs/base/browser/ui/button/button';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { HistoryNavigator } from 'vs/base/common/history';
|
||||
import { HistoryNavigator2 } from 'vs/base/common/history';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { basename, dirname } from 'vs/base/common/path';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
|
||||
|
@ -21,6 +23,7 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
|
|||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget';
|
||||
import { IDimension } from 'vs/editor/common/core/dimension';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IModelService } from 'vs/editor/common/services/model';
|
||||
import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController';
|
||||
|
@ -38,6 +41,7 @@ import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/b
|
|||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
|
@ -53,9 +57,6 @@ import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService';
|
|||
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
|
||||
import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
|
||||
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { basename, dirname } from 'vs/base/common/path';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
|
@ -130,10 +131,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
return this._inputEditor;
|
||||
}
|
||||
|
||||
private history: HistoryNavigator<IChatHistoryEntry>;
|
||||
private history: HistoryNavigator2<IChatHistoryEntry>;
|
||||
private historyNavigationBackwardsEnablement!: IContextKey<boolean>;
|
||||
private historyNavigationForewardsEnablement!: IContextKey<boolean>;
|
||||
private onHistoryEntry = false;
|
||||
private inHistoryNavigation = false;
|
||||
private inputModel: ITextModel | undefined;
|
||||
private inputEditorHasText: IContextKey<boolean>;
|
||||
|
@ -156,6 +156,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
@ -165,8 +166,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService);
|
||||
this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService);
|
||||
|
||||
this.history = new HistoryNavigator([], 5);
|
||||
this._register(this.historyService.onDidClearHistory(() => this.history.clear()));
|
||||
this.history = this.loadHistory();
|
||||
this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn)));
|
||||
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) {
|
||||
|
@ -175,6 +176,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
}));
|
||||
}
|
||||
|
||||
private loadHistory(): HistoryNavigator2<IChatHistoryEntry> {
|
||||
const history = this.historyService.getHistory(this.location);
|
||||
if (history.length === 0) {
|
||||
history.push({ text: '' });
|
||||
}
|
||||
|
||||
return new HistoryNavigator2(history, 50, historyKeyFn);
|
||||
}
|
||||
|
||||
private _getAriaLabel(): string {
|
||||
const verbose = this.configurationService.getValue<boolean>(AccessibilityVerbositySettingId.Chat);
|
||||
if (verbose) {
|
||||
|
@ -184,13 +194,35 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
return localize('chatInput', "Chat Input");
|
||||
}
|
||||
|
||||
setState(inputValue: string | undefined): void {
|
||||
const history = this.historyService.getHistory(this.location);
|
||||
this.history = new HistoryNavigator(history, 50);
|
||||
|
||||
if (typeof inputValue === 'string') {
|
||||
this.setValue(inputValue);
|
||||
updateState(inputState: Object): void {
|
||||
if (this.inHistoryNavigation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newEntry = { text: this._inputEditor.getValue(), state: inputState };
|
||||
|
||||
if (this.history.isAtEnd()) {
|
||||
// The last history entry should always be the current input value
|
||||
this.history.replaceLast(newEntry);
|
||||
} else {
|
||||
// Added a reference while in the middle of history navigation, it's a new entry
|
||||
this.history.replaceLast(newEntry);
|
||||
this.history.resetCursor();
|
||||
}
|
||||
}
|
||||
|
||||
initForNewChatModel(inputValue: string | undefined, inputState: Object): void {
|
||||
this.history = this.loadHistory();
|
||||
this.history.add({ text: inputValue ?? this.history.current().text, state: inputState });
|
||||
|
||||
if (inputValue) {
|
||||
this.setValue(inputValue, false);
|
||||
}
|
||||
}
|
||||
|
||||
logInputHistory(): void {
|
||||
const historyStr = [...this.history].map(entry => JSON.stringify(entry)).join('\n');
|
||||
this.logService.info(`[${this.location}] Chat input history:`, historyStr);
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): void {
|
||||
|
@ -202,24 +234,39 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
}
|
||||
|
||||
showPreviousValue(): void {
|
||||
if (this.history.isAtEnd()) {
|
||||
this.saveCurrentValue();
|
||||
} else {
|
||||
if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) {
|
||||
this.saveCurrentValue();
|
||||
this.history.resetCursor();
|
||||
}
|
||||
}
|
||||
|
||||
this.navigateHistory(true);
|
||||
}
|
||||
|
||||
showNextValue(): void {
|
||||
if (this.history.isAtEnd()) {
|
||||
return;
|
||||
} else {
|
||||
if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) {
|
||||
this.saveCurrentValue();
|
||||
this.history.resetCursor();
|
||||
}
|
||||
}
|
||||
|
||||
this.navigateHistory(false);
|
||||
}
|
||||
|
||||
private navigateHistory(previous: boolean): void {
|
||||
const historyEntry = (previous ?
|
||||
(this.history.previous() ?? this.history.first()) : this.history.next())
|
||||
?? { text: '' };
|
||||
|
||||
this.onHistoryEntry = previous || this.history.current() !== null;
|
||||
const historyEntry = previous ?
|
||||
this.history.previous() : this.history.next();
|
||||
|
||||
aria.status(historyEntry.text);
|
||||
|
||||
this.inHistoryNavigation = true;
|
||||
this.setValue(historyEntry.text);
|
||||
this.setValue(historyEntry.text, true);
|
||||
this.inHistoryNavigation = false;
|
||||
|
||||
this._onDidLoadInputState.fire(historyEntry.state);
|
||||
|
@ -235,10 +282,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
}
|
||||
}
|
||||
|
||||
setValue(value: string): void {
|
||||
setValue(value: string, transient: boolean): void {
|
||||
this.inputEditor.setValue(value);
|
||||
// always leave cursor at the end
|
||||
this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
|
||||
|
||||
if (!transient) {
|
||||
this.saveCurrentValue();
|
||||
}
|
||||
}
|
||||
|
||||
private saveCurrentValue(): void {
|
||||
const newEntry = { text: this._inputEditor.getValue(), state: this.history.current().state };
|
||||
this.history.replaceLast(newEntry);
|
||||
}
|
||||
|
||||
focus() {
|
||||
|
@ -253,17 +309,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
* Reset the input and update history.
|
||||
* @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed.
|
||||
*/
|
||||
async acceptInput(userQuery?: string, inputState?: any): Promise<void> {
|
||||
if (userQuery) {
|
||||
let element = this.history.getHistory().find(candidate => candidate.text === userQuery);
|
||||
if (!element) {
|
||||
element = { text: userQuery, state: inputState };
|
||||
} else {
|
||||
element.state = inputState;
|
||||
}
|
||||
this.history.add(element);
|
||||
async acceptInput(isUserQuery?: boolean): Promise<void> {
|
||||
if (isUserQuery) {
|
||||
const userQuery = this._inputEditor.getValue();
|
||||
const entry: IChatHistoryEntry = { text: userQuery, state: this.history.current().state };
|
||||
this.history.replaceLast(entry);
|
||||
this.history.add({ text: '' });
|
||||
}
|
||||
|
||||
this._onDidLoadInputState.fire({});
|
||||
if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) {
|
||||
this._acceptInputForVoiceover();
|
||||
} else {
|
||||
|
@ -343,21 +397,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
this._onDidChangeHeight.fire();
|
||||
}
|
||||
|
||||
// Only allow history navigation when the input is empty.
|
||||
// (If this model change happened as a result of a history navigation, this is canceled out by a call in this.navigateHistory)
|
||||
const model = this._inputEditor.getModel();
|
||||
const inputHasText = !!model && model.getValue().trim().length > 0;
|
||||
this.inputEditorHasText.set(inputHasText);
|
||||
|
||||
// If the user is typing on a history entry, then reset the onHistoryEntry flag so that history navigation can be disabled
|
||||
if (!this.inHistoryNavigation) {
|
||||
this.onHistoryEntry = false;
|
||||
}
|
||||
|
||||
if (!this.onHistoryEntry) {
|
||||
this.historyNavigationForewardsEnablement.set(!inputHasText);
|
||||
this.historyNavigationBackwardsEnablement.set(!inputHasText);
|
||||
}
|
||||
}));
|
||||
this._register(this._inputEditor.onDidFocusEditorText(() => {
|
||||
this.inputEditorHasFocus.set(true);
|
||||
|
@ -379,10 +421,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
const atTop = e.position.column === 1 && e.position.lineNumber === 1;
|
||||
this.chatCursorAtTop.set(atTop);
|
||||
|
||||
if (this.onHistoryEntry) {
|
||||
this.historyNavigationBackwardsEnablement.set(atTop);
|
||||
this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model)));
|
||||
}
|
||||
this.historyNavigationBackwardsEnablement.set(atTop);
|
||||
this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model)));
|
||||
}));
|
||||
|
||||
this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputContainer, this.options.menus.executeToolbar, {
|
||||
|
@ -568,11 +608,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
|||
}
|
||||
|
||||
saveState(): void {
|
||||
const inputHistory = this.history.getHistory();
|
||||
const inputHistory = [...this.history];
|
||||
this.historyService.saveHistory(this.location, inputHistory);
|
||||
}
|
||||
}
|
||||
|
||||
const historyKeyFn = (entry: IChatHistoryEntry) => JSON.stringify(entry);
|
||||
|
||||
function getLastPosition(model: ITextModel): IPosition {
|
||||
return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 };
|
||||
}
|
||||
|
|
|
@ -69,6 +69,8 @@ export interface IChatWidgetContrib extends IDisposable {
|
|||
*/
|
||||
getInputState?(): any;
|
||||
|
||||
onDidChangeInputState?: Event<void>;
|
||||
|
||||
/**
|
||||
* Called with the result of getInputState when navigating input history.
|
||||
*/
|
||||
|
@ -111,7 +113,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
private readonly _onDidChangeContentHeight = new Emitter<void>();
|
||||
readonly onDidChangeContentHeight: Event<void> = this._onDidChangeContentHeight.event;
|
||||
|
||||
private contribs: IChatWidgetContrib[] = [];
|
||||
private contribs: ReadonlyArray<IChatWidgetContrib> = [];
|
||||
|
||||
private tree!: WorkbenchObjectTree<ChatTreeItem>;
|
||||
private renderer!: ChatListItemRenderer;
|
||||
|
@ -311,6 +313,15 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
return undefined;
|
||||
}
|
||||
}).filter(isDefined);
|
||||
|
||||
this.contribs.forEach(c => {
|
||||
if (c.onDidChangeInputState) {
|
||||
this._register(c.onDidChangeInputState(() => {
|
||||
const state = this.collectInputState();
|
||||
this.inputPart.updateState(state);
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getContrib<T extends IChatWidgetContrib>(id: string): T | undefined {
|
||||
|
@ -552,8 +563,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
|
||||
this._register(this.inputPart.onDidLoadInputState(state => {
|
||||
this.contribs.forEach(c => {
|
||||
if (c.setInputState && typeof state === 'object' && state?.[c.id]) {
|
||||
c.setInputState(state[c.id]);
|
||||
if (c.setInputState) {
|
||||
const contribState = (typeof state === 'object' && state?.[c.id]) ?? {};
|
||||
c.setInputState(contribState);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
@ -646,7 +658,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
this.viewModel = undefined;
|
||||
this.onDidChangeItems();
|
||||
}));
|
||||
this.inputPart.setState(viewState.inputValue);
|
||||
this.inputPart.initForNewChatModel(viewState.inputValue, viewState.inputState ?? this.collectInputState());
|
||||
this.contribs.forEach(c => {
|
||||
if (c.setInputState && viewState.inputState?.[c.id]) {
|
||||
c.setInputState(viewState.inputState?.[c.id]);
|
||||
|
@ -692,13 +704,17 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
}
|
||||
|
||||
setInput(value = ''): void {
|
||||
this.inputPart.setValue(value);
|
||||
this.inputPart.setValue(value, false);
|
||||
}
|
||||
|
||||
getInput(): string {
|
||||
return this.inputPart.inputEditor.getValue();
|
||||
}
|
||||
|
||||
logInputHistory(): void {
|
||||
this.inputPart.logInputHistory();
|
||||
}
|
||||
|
||||
async acceptInput(query?: string): Promise<IChatResponseModel | undefined> {
|
||||
return this._acceptInput(query ? { query } : undefined);
|
||||
}
|
||||
|
@ -731,9 +747,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
|||
|
||||
if (result) {
|
||||
this.inputPart.attachedContext.clear();
|
||||
const inputState = this.collectInputState();
|
||||
this.inputPart.acceptInput(isUserQuery ? input : undefined, isUserQuery ? inputState : undefined);
|
||||
this.inputPart.acceptInput(isUserQuery);
|
||||
this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand });
|
||||
this.inputPart.updateState(this.collectInputState());
|
||||
result.responseCompletePromise.then(() => {
|
||||
const responses = this.viewModel?.getItems().filter(isResponseVM);
|
||||
const lastResponse = responses?.[responses.length - 1];
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat';
|
||||
import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget';
|
||||
|
@ -12,6 +13,9 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon
|
|||
|
||||
private _attachedContext = new Set<IChatRequestVariableEntry>();
|
||||
|
||||
private readonly _onDidChangeInputState = this._register(new Emitter<void>());
|
||||
readonly onDidChangeInputState = this._onDidChangeInputState.event;
|
||||
|
||||
public static readonly ID = 'chatContextAttachments';
|
||||
|
||||
get id() {
|
||||
|
@ -30,13 +34,18 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon
|
|||
}));
|
||||
}
|
||||
|
||||
getInputState?() {
|
||||
getInputState(): IChatRequestVariableEntry[] {
|
||||
return [...this._attachedContext.values()];
|
||||
}
|
||||
|
||||
setInputState?(s: any): void {
|
||||
setInputState(s: any): void {
|
||||
if (!Array.isArray(s)) {
|
||||
return;
|
||||
s = [];
|
||||
}
|
||||
|
||||
this._attachedContext.clear();
|
||||
for (const attachment of s) {
|
||||
this._attachedContext.add(attachment);
|
||||
}
|
||||
|
||||
this.widget.setContext(true, ...s);
|
||||
|
@ -55,10 +64,12 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon
|
|||
}
|
||||
|
||||
this.widget.setContext(overwrite, ...attachments);
|
||||
this._onDidChangeInputState.fire();
|
||||
}
|
||||
|
||||
private _removeContext(attachment: IChatRequestVariableEntry) {
|
||||
this._attachedContext.delete(attachment);
|
||||
this._onDidChangeInputState.fire();
|
||||
}
|
||||
|
||||
private _clearAttachedContext() {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
|
@ -38,24 +39,30 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC
|
|||
return ChatDynamicVariableModel.ID;
|
||||
}
|
||||
|
||||
private _onDidChangeInputState = this._register(new Emitter<void>());
|
||||
readonly onDidChangeInputState = this._onDidChangeInputState.event;
|
||||
|
||||
constructor(
|
||||
private readonly widget: IChatWidget,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
this._register(widget.inputEditor.onDidChangeModelContent(e => {
|
||||
e.changes.forEach(c => {
|
||||
// Don't mutate entries in _variables, since they will be returned from the getter
|
||||
const originalNumVariables = this._variables.length;
|
||||
this._variables = coalesce(this._variables.map(ref => {
|
||||
const intersection = Range.intersectRanges(ref.range, c.range);
|
||||
if (intersection && !intersection.isEmpty()) {
|
||||
// The reference text was changed, it's broken
|
||||
const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1);
|
||||
this.widget.inputEditor.executeEdits(this.id, [{
|
||||
range: rangeToDelete,
|
||||
text: '',
|
||||
}]);
|
||||
// The reference text was changed, it's broken.
|
||||
// But if the whole reference range was deleted (eg history navigation) then don't try to change the editor.
|
||||
if (!Range.containsRange(c.range, ref.range)) {
|
||||
const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1);
|
||||
this.widget.inputEditor.executeEdits(this.id, [{
|
||||
range: rangeToDelete,
|
||||
text: '',
|
||||
}]);
|
||||
}
|
||||
return null;
|
||||
} else if (Range.compareRangesUsingStarts(ref.range, c.range) > 0) {
|
||||
const delta = c.text.length - c.rangeLength;
|
||||
|
@ -72,6 +79,10 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC
|
|||
|
||||
return ref;
|
||||
}));
|
||||
|
||||
if (this._variables.length !== originalNumVariables) {
|
||||
this._onDidChangeInputState.fire();
|
||||
}
|
||||
});
|
||||
|
||||
this.updateDecorations();
|
||||
|
@ -84,9 +95,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC
|
|||
|
||||
setInputState(s: any): void {
|
||||
if (!Array.isArray(s)) {
|
||||
// Something went wrong
|
||||
this.logService.warn('ChatDynamicVariableModel.setInputState called with invalid state: ' + JSON.stringify(s));
|
||||
return;
|
||||
s = [];
|
||||
}
|
||||
|
||||
this._variables = s;
|
||||
|
@ -96,6 +105,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC
|
|||
addReference(ref: IDynamicVariable): void {
|
||||
this._variables.push(ref);
|
||||
this.updateDecorations();
|
||||
this._onDidChangeInputState.fire();
|
||||
}
|
||||
|
||||
private updateDecorations(): void {
|
||||
|
|
|
@ -464,7 +464,7 @@ export class InlineChatWidget {
|
|||
*/
|
||||
addToHistory(input: string) {
|
||||
if (this._chatWidget.viewModel?.model === this._defaultChatModel) {
|
||||
this._chatWidget.input.acceptInput(input);
|
||||
this._chatWidget.input.acceptInput(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -134,7 +134,7 @@ class SCMInput extends Disposable implements ISCMInput {
|
|||
}
|
||||
|
||||
if (!transient) {
|
||||
this.historyNavigator.add(this._value);
|
||||
this.historyNavigator.replaceLast(this._value);
|
||||
this.historyNavigator.add(value);
|
||||
this.didChangeHistory = true;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue