Skip to content

Commit c9d8ab6

Browse files
Add two alternative UX's for the Quick Question experience (microsoft#187780)
* Add two alternative UX's for the Quick Question experience Main changes: * Changes to the chat to have an alterative rendering of the input on the top... needs polish but decent for now * The inclusion of a setting to choose which mode you would like to use for the UX * 2 new modes: `InputOnBottomChat` and `InputOnTopChat` which do what you expect. * Move where the enum is to hopefully fix tests
1 parent 7123ddb commit c9d8ab6

File tree

10 files changed

+627
-286
lines changed

10 files changed

+627
-286
lines changed

src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts

Lines changed: 4 additions & 276 deletions
Original file line numberDiff line numberDiff line change
@@ -3,284 +3,12 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as dom from 'vs/base/browser/dom';
7-
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
8-
import { CancellationToken } from 'vs/base/common/cancellation';
9-
import { Codicon } from 'vs/base/common/codicons';
10-
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
11-
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
12-
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
13-
import { localize } from 'vs/nls';
14-
import { Emitter, Event } from 'vs/base/common/event';
15-
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
16-
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
17-
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
18-
import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
19-
import { asCssVariable, editorBackground, foreground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry';
20-
import { ChatListItemRenderer } from 'vs/workbench/contrib/chat/browser/chatListRenderer';
21-
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
22-
import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
23-
import { IChatReplyFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
24-
import { ChatViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
25-
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
26-
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
27-
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
6+
import { registerAction2 } from 'vs/platform/actions/common/actions';
7+
import { AskQuickQuestionAction } from 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction';
288

29-
export const ASK_QUICK_QUESTION_ACTION_ID = 'chat.action.askQuickQuestion';
9+
import 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction';
10+
import 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction';
3011

3112
export function registerChatQuickQuestionActions() {
3213
registerAction2(AskQuickQuestionAction);
3314
}
34-
35-
class AskQuickQuestionAction extends Action2 {
36-
37-
private _currentSession: InteractiveQuickPickSession | undefined;
38-
private _currentQuery: string | undefined;
39-
private _lastAcceptedQuery: string | undefined;
40-
private _currentTimer: any | undefined;
41-
private _input: IQuickPick<IQuickPickItem> | undefined;
42-
43-
constructor() {
44-
super({
45-
id: ASK_QUICK_QUESTION_ACTION_ID,
46-
title: { value: localize('askQuickQuestion', "Ask Quick Question"), original: 'Ask Quick Question' },
47-
precondition: CONTEXT_PROVIDER_EXISTS,
48-
f1: false,
49-
category: CHAT_CATEGORY,
50-
keybinding: {
51-
weight: KeybindingWeight.WorkbenchContrib,
52-
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
53-
linux: {
54-
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI
55-
}
56-
}
57-
});
58-
}
59-
60-
run(accessor: ServicesAccessor, query: string): void {
61-
const quickInputService = accessor.get(IQuickInputService);
62-
const chatService = accessor.get(IChatService);
63-
const instantiationService = accessor.get(IInstantiationService);
64-
65-
// First things first, clear the existing timer that will dispose the session
66-
clearTimeout(this._currentTimer);
67-
this._currentTimer = undefined;
68-
69-
// If the input is already shown, hide it. This provides a toggle behavior of the quick pick
70-
if (this._input !== undefined) {
71-
this._input.hide();
72-
return;
73-
}
74-
75-
// Check if any providers are available. If not, show nothing
76-
const providerInfo = chatService.getProviderInfos()[0];
77-
if (!providerInfo) {
78-
return;
79-
}
80-
81-
const disposableStore = new DisposableStore();
82-
83-
//#region Setup quick pick
84-
85-
this._input = quickInputService.createQuickPick();
86-
disposableStore.add(this._input);
87-
this._input.placeholder = localize('askabot', "Ask {0} a question...", providerInfo.displayName);
88-
89-
// Setup toggle that will be used to open the chat view
90-
const openInChat = new Toggle({
91-
title: 'Open in chat view',
92-
icon: Codicon.commentDiscussion,
93-
isChecked: false,
94-
inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder),
95-
inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground),
96-
inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground)
97-
});
98-
disposableStore.add(openInChat);
99-
disposableStore.add(openInChat.onChange(async () => {
100-
await this._currentSession?.openInChat(this._lastAcceptedQuery ?? this._input!.value);
101-
this._currentQuery = undefined;
102-
this._lastAcceptedQuery = undefined;
103-
this._currentSession?.dispose();
104-
this._currentSession = undefined;
105-
}));
106-
this._input.toggles = [openInChat];
107-
108-
// Setup the widget that will be used to render the chat response
109-
const containerList = dom.$('.interactive-list');
110-
const containerSession = dom.$('.interactive-session', undefined, containerList);
111-
containerList.style.position = 'relative';
112-
this._input.widget = containerSession;
113-
114-
//#endregion
115-
116-
//#region quick pick events
117-
118-
disposableStore.add(this._input.onDidChangeValue((value) => {
119-
if (value !== this._currentQuery) {
120-
this._currentQuery = value;
121-
}
122-
}));
123-
disposableStore.add(this._input.onDidHide(() => {
124-
disposableStore.dispose();
125-
this._input = undefined;
126-
this._currentTimer = setTimeout(() => {
127-
this._currentQuery = undefined;
128-
this._lastAcceptedQuery = undefined;
129-
this._currentSession?.dispose();
130-
this._currentSession = undefined;
131-
}, 1000 * 30); // 30 seconds
132-
}));
133-
disposableStore.add(this._input.onDidAccept(async () => {
134-
await this._currentSession?.accept(this._input!.value);
135-
this._lastAcceptedQuery = this._input!.value;
136-
}));
137-
138-
//#endregion
139-
140-
// If we were given a query (via executeCommand), then clear past state
141-
if (query) {
142-
this._currentSession?.dispose();
143-
this._currentSession = undefined;
144-
}
145-
this._currentSession ??= instantiationService.createInstance(InteractiveQuickPickSession);
146-
this._input.show();
147-
// This line must come after showing the input so the offsetWidth is correct
148-
this._currentSession.createList(containerList, containerList.offsetWidth);
149-
150-
disposableStore.add(this._currentSession.onDidClickFollowup(async e => {
151-
this._input!.focusOnInput();
152-
this._input!.value = e.message;
153-
await this._currentSession?.accept(e.message);
154-
}));
155-
156-
// If we were given a query (via executeCommand), then accept it
157-
if (query) {
158-
this._input.value = query;
159-
this._input.valueSelection = [0, this._input.value.length];
160-
this._currentQuery = query;
161-
this._currentSession.accept(query);
162-
} else if (this._currentQuery) {
163-
this._input.value = this._currentQuery;
164-
this._input.valueSelection = [0, this._input.value.length];
165-
}
166-
}
167-
}
168-
169-
class InteractiveQuickPickSession extends Disposable {
170-
171-
private _model: ChatModel;
172-
private _viewModel: ChatViewModel;
173-
174-
private readonly _onDidClickFollowup: Emitter<IChatReplyFollowup> = this._register(new Emitter<IChatReplyFollowup>());
175-
onDidClickFollowup: Event<IChatReplyFollowup> = this._onDidClickFollowup.event;
176-
177-
private _listDisposable: DisposableStore | undefined;
178-
179-
constructor(
180-
@IInstantiationService private readonly _instantiationService: IInstantiationService,
181-
@IChatService private readonly _chatService: IChatService,
182-
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService
183-
) {
184-
super();
185-
186-
const providerInfo = _chatService.getProviderInfos()[0];
187-
this._model = this._register(_chatService.startSession(providerInfo.id, CancellationToken.None)!);
188-
this._viewModel = this._register(new ChatViewModel(this._model, _instantiationService));
189-
}
190-
191-
createList(container: HTMLElement, offsetWidth: number) {
192-
this._listDisposable?.dispose();
193-
this._listDisposable = new DisposableStore();
194-
const options = this._listDisposable.add(this._instantiationService.createInstance(ChatEditorOptions, 'quickpick-interactive', foreground, editorBackground, editorBackground));
195-
const list = this._listDisposable.add(this._instantiationService.createInstance(
196-
ChatListItemRenderer,
197-
options,
198-
{
199-
getListLength: () => {
200-
return 1;
201-
},
202-
getSlashCommands() {
203-
return [];
204-
},
205-
}
206-
));
207-
208-
const template = list.renderTemplate(container);
209-
list.layout(offsetWidth);
210-
this._listDisposable.add(this._viewModel.onDidChange(() => {
211-
const items = this._viewModel.getItems();
212-
const node = {
213-
element: items[items.length - 1],
214-
children: [],
215-
collapsed: false,
216-
collapsible: false,
217-
depth: 0,
218-
filterData: undefined,
219-
visible: true,
220-
visibleChildIndex: 0,
221-
visibleChildrenCount: 1,
222-
};
223-
list.disposeElement(node, 0, template);
224-
list.renderElement(node, 0, template);
225-
}));
226-
227-
if (this._viewModel.getItems().length) {
228-
const items = this._viewModel.getItems();
229-
const node = {
230-
element: items[items.length - 1],
231-
children: [],
232-
collapsed: false,
233-
collapsible: false,
234-
depth: 0,
235-
filterData: undefined,
236-
visible: true,
237-
visibleChildIndex: 0,
238-
visibleChildrenCount: 1,
239-
};
240-
list.disposeElement(node, 0, template);
241-
list.renderElement(node, 0, template);
242-
}
243-
244-
this._listDisposable.add(list.onDidClickFollowup(e => {
245-
this._onDidClickFollowup.fire(e);
246-
}));
247-
}
248-
249-
get providerId() {
250-
return this._model.providerId;
251-
}
252-
253-
get sessionId() {
254-
return this._model.sessionId;
255-
}
256-
257-
async accept(query: string) {
258-
await this._model.waitForInitialization();
259-
const requests = this._model.getRequests();
260-
const lastRequest = requests[requests.length - 1];
261-
if (lastRequest?.message && lastRequest?.message === query) {
262-
return;
263-
}
264-
if (this._model.requestInProgress) {
265-
this._chatService.cancelCurrentRequestForSession(this.sessionId);
266-
}
267-
await this._chatService.sendRequest(this.sessionId, query);
268-
}
269-
270-
async openInChat(value: string) {
271-
const widget = await this._chatWidgetService.revealViewForProvider(this.providerId);
272-
if (!widget?.viewModel) {
273-
return;
274-
}
275-
276-
const requests = this._model.getRequests().reverse();
277-
const response = requests.find(r => r.response?.response.value !== undefined);
278-
const message = response?.response?.response.value;
279-
if (message) {
280-
this._chatService.addCompleteRequest(widget.viewModel.sessionId, value, { message });
281-
} else if (value) {
282-
this._chatService.sendRequest(widget.viewModel.sessionId, value);
283-
}
284-
widget.focusInput();
285-
}
286-
}

0 commit comments

Comments
 (0)