Skip to content

Commit 08e17e0

Browse files
roblourensNikolaRHristov
authored andcommitted
Merge pull request microsoft#247496 from microsoft/roblou/straightforward-bass
Add per-chatMode actions and keybindings
2 parents 8200bac + d6b7cf8 commit 08e17e0

File tree

9 files changed

+153
-92
lines changed

9 files changed

+153
-92
lines changed

src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ActionListItemKind, IActionListDelegate, IActionListItem } from './acti
1010
import { ThemeIcon } from '../../../base/common/themables.js';
1111
import { Codicon } from '../../../base/common/codicons.js';
1212
import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js';
13+
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
1314

1415
export interface IActionWidgetDropdownAction extends IAction {
1516
category?: { label: string; order: number };
@@ -40,6 +41,7 @@ export class ActionWidgetDropdown extends BaseDropdown {
4041
container: HTMLElement,
4142
private readonly _options: IActionWidgetDropdownOptions,
4243
@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,
44+
@IKeybindingService private readonly keybindingService: IKeybindingService,
4345
) {
4446
super(container, _options);
4547
}
@@ -93,6 +95,7 @@ export class ActionWidgetDropdown extends BaseDropdown {
9395
disabled: false,
9496
hideIcon: false,
9597
label: action.label,
98+
keybinding: this.keybindingService.lookupKeybinding(action.id)
9699
});
97100
}
98101
}

src/vs/platform/actions/browser/actionWidgetDropdownActionViewItem.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class ActionWidgetDropdownActionViewItem extends BaseActionViewItem {
4040
return this.renderLabel(this.element);
4141
};
4242

43-
this.actionWidgetDropdown = this._register(new ActionWidgetDropdown(container, { ...this.actionWidgetOptions, labelRenderer }, this._actionWidgetService));
43+
this.actionWidgetDropdown = this._register(new ActionWidgetDropdown(container, { ...this.actionWidgetOptions, labelRenderer }, this._actionWidgetService, this._keybindingService));
4444
this._register(this.actionWidgetDropdown.onDidChangeVisibility(visible => {
4545
this.element?.setAttribute('aria-expanded', `${visible}`);
4646
}));

src/vs/platform/actions/common/actions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ interface IBaseAction2Options extends IAction2CommonOptions {
604604
f1?: false;
605605
}
606606

607-
interface ICommandPaletteOptions extends IAction2CommonOptions {
607+
export interface ICommandPaletteOptions extends IAction2CommonOptions {
608608

609609
/**
610610
* The title of the command that will be displayed in the command palette after the category.

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

+123-70
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { isAncestorOfActiveElement } from '../../../../../base/browser/dom.js';
67
import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';
78
import { coalesce } from '../../../../../base/common/arrays.js';
89
import { Codicon } from '../../../../../base/common/codicons.js';
@@ -21,7 +22,7 @@ import { SuggestController } from '../../../../../editor/contrib/suggest/browser
2122
import { localize, localize2 } from '../../../../../nls.js';
2223
import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
2324
import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js';
24-
import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js';
25+
import { Action2, ICommandPaletteOptions, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js';
2526
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
2627
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
2728
import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js';
@@ -50,7 +51,7 @@ import { extractAgentAndCommand } from '../../common/chatParserTypes.js';
5051
import { IChatDetail, IChatService } from '../../common/chatService.js';
5152
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js';
5253
import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js';
53-
import { ChatMode, validateChatMode } from '../../common/constants.js';
54+
import { ChatConfiguration, ChatMode, modeToString, validateChatMode } from '../../common/constants.js';
5455
import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';
5556
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
5657
import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js';
@@ -100,88 +101,140 @@ export interface IChatViewOpenRequestEntry {
100101

101102
const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog';
102103

103-
export function registerChatActions() {
104-
registerAction2(class OpenChatGlobalAction extends Action2 {
104+
abstract class OpenChatGlobalAction extends Action2 {
105+
constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: ChatMode) {
106+
super({
107+
...overrides,
108+
icon: Codicon.copilot,
109+
f1: true,
110+
category: CHAT_CATEGORY,
111+
precondition: ChatContextKeys.Setup.hidden.negate(),
112+
});
113+
}
105114

106-
constructor() {
107-
super({
108-
id: CHAT_OPEN_ACTION_ID,
109-
title: localize2('openChat', "Open Chat"),
110-
icon: Codicon.copilot,
111-
f1: true,
112-
category: CHAT_CATEGORY,
113-
precondition: ChatContextKeys.Setup.hidden.negate(),
114-
keybinding: {
115-
weight: KeybindingWeight.WorkbenchContrib,
116-
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
117-
mac: {
118-
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
119-
}
120-
},
121-
menu: [{
122-
id: MenuId.ChatTitleBarMenu,
123-
group: 'a_open',
124-
order: 1
125-
}]
126-
});
127-
}
115+
override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise<void> {
116+
opts = typeof opts === 'string' ? { query: opts } : opts;
128117

129-
override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise<void> {
130-
opts = typeof opts === 'string' ? { query: opts } : opts;
118+
const chatService = accessor.get(IChatService);
119+
const widgetService = accessor.get(IChatWidgetService);
120+
const toolsService = accessor.get(ILanguageModelToolsService);
121+
const viewsService = accessor.get(IViewsService);
122+
const hostService = accessor.get(IHostService);
131123

132-
const chatService = accessor.get(IChatService);
133-
const toolsService = accessor.get(ILanguageModelToolsService);
134-
const viewsService = accessor.get(IViewsService);
135-
const hostService = accessor.get(IHostService);
136124

137-
const chatWidget = await showChatView(viewsService);
138-
if (!chatWidget) {
139-
return;
125+
let chatWidget = widgetService.lastFocusedWidget;
126+
// When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one.
127+
// Otherwise, open the view.
128+
if (!this.mode || !chatWidget || !isAncestorOfActiveElement(chatWidget.domNode)) {
129+
chatWidget = await showChatView(viewsService);
130+
}
131+
132+
if (!chatWidget) {
133+
return;
134+
}
135+
136+
const mode = opts?.mode ?? this.mode;
137+
if (mode && validateChatMode(mode)) {
138+
chatWidget.input.setChatMode(mode);
139+
}
140+
if (opts?.previousRequests?.length && chatWidget.viewModel) {
141+
for (const { request, response } of opts.previousRequests) {
142+
chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response });
140143
}
141-
if (opts?.mode && validateChatMode(opts.mode)) {
142-
chatWidget.input.setChatMode(opts.mode);
144+
}
145+
if (opts?.attachScreenshot) {
146+
const screenshot = await hostService.getScreenshot();
147+
if (screenshot) {
148+
chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot));
143149
}
144-
if (opts?.previousRequests?.length && chatWidget.viewModel) {
145-
for (const { request, response } of opts.previousRequests) {
146-
chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response });
147-
}
150+
}
151+
if (opts?.query) {
152+
if (opts.query.startsWith('@') && (chatWidget.input.currentMode === ChatMode.Agent || chatService.edits2Enabled)) {
153+
chatWidget.input.setChatMode(ChatMode.Ask);
148154
}
149-
if (opts?.attachScreenshot) {
150-
const screenshot = await hostService.getScreenshot();
151-
if (screenshot) {
152-
chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot));
153-
}
155+
if (opts.isPartialQuery) {
156+
chatWidget.setInput(opts.query);
157+
} else {
158+
await chatWidget.waitForReady();
159+
chatWidget.acceptInput(opts.query);
154160
}
155-
if (opts?.query) {
156-
if (opts.query.startsWith('@') && (chatWidget.input.currentMode === ChatMode.Agent || chatService.edits2Enabled)) {
157-
chatWidget.input.setChatMode(ChatMode.Ask);
158-
}
159-
if (opts.isPartialQuery) {
160-
chatWidget.setInput(opts.query);
161-
} else {
162-
await chatWidget.waitForReady();
163-
chatWidget.acceptInput(opts.query);
161+
}
162+
if (opts?.toolIds && opts.toolIds.length > 0) {
163+
for (const toolId of opts.toolIds) {
164+
const tool = toolsService.getTool(toolId);
165+
if (tool) {
166+
chatWidget.attachmentModel.addContext({
167+
id: tool.id,
168+
name: tool.displayName,
169+
fullName: tool.displayName,
170+
value: undefined,
171+
icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined,
172+
kind: 'tool'
173+
});
164174
}
165175
}
166-
if (opts?.toolIds && opts.toolIds.length > 0) {
167-
for (const toolId of opts.toolIds) {
168-
const tool = toolsService.getTool(toolId);
169-
if (tool) {
170-
chatWidget.attachmentModel.addContext({
171-
id: tool.id,
172-
name: tool.displayName,
173-
fullName: tool.displayName,
174-
value: undefined,
175-
icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined,
176-
kind: 'tool'
177-
});
178-
}
176+
}
177+
178+
chatWidget.focusInput();
179+
}
180+
}
181+
182+
class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction {
183+
constructor() {
184+
super({
185+
id: CHAT_OPEN_ACTION_ID,
186+
title: localize2('openChat', "Open Chat"),
187+
keybinding: {
188+
weight: KeybindingWeight.WorkbenchContrib,
189+
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
190+
mac: {
191+
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
179192
}
180-
}
193+
},
194+
menu: [{
195+
id: MenuId.ChatTitleBarMenu,
196+
group: 'a_open',
197+
order: 1
198+
}]
199+
});
200+
}
201+
}
202+
203+
export function getOpenChatActionIdForMode(mode: ChatMode): string {
204+
const modeStr = modeToString(mode);
205+
return `workbench.action.chat.open${modeStr}`;
206+
}
207+
208+
abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction {
209+
constructor(mode: ChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {
210+
super({
211+
id: getOpenChatActionIdForMode(mode),
212+
title: localize2('openChatMode', "Open Chat ({0})", modeToString(mode)),
213+
keybinding
214+
}, mode);
215+
}
216+
}
181217

182-
chatWidget.focusInput();
218+
export function registerChatActions() {
219+
registerAction2(PrimaryOpenChatGlobalAction);
220+
registerAction2(class extends ModeOpenChatGlobalAction {
221+
constructor() { super(ChatMode.Ask); }
222+
});
223+
registerAction2(class extends ModeOpenChatGlobalAction {
224+
constructor() {
225+
super(ChatMode.Agent, {
226+
when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentEnabled}`),
227+
weight: KeybindingWeight.WorkbenchContrib,
228+
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
229+
linux: {
230+
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI
231+
}
232+
},);
183233
}
184234
});
235+
registerAction2(class extends ModeOpenChatGlobalAction {
236+
constructor() { super(ChatMode.Edit); }
237+
});
185238

186239
registerAction2(class ToggleChatAction extends Action2 {
187240
constructor() {

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ export function registerNewChatActions() {
6868
}
6969
});
7070

71-
registerAction2(class NewEditSessionAction extends EditingSessionAction {
71+
registerAction2(class NewChatAction extends EditingSessionAction {
7272
constructor() {
7373
super({
74-
id: ACTION_ID_NEW_EDIT_SESSION,
74+
id: ACTION_ID_NEW_CHAT,
7575
title: localize2('chat.newEdits.label', "New Chat"),
7676
category: CHAT_CATEGORY,
7777
icon: Codicon.plus,
@@ -134,7 +134,7 @@ export function registerNewChatActions() {
134134
}
135135
}
136136
});
137-
CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION);
137+
CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT);
138138

139139

140140
registerAction2(class UndoChatEditInteractionAction extends EditingSessionAction {

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

+1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export interface IChatAcceptInputOptions {
169169
}
170170

171171
export interface IChatWidget {
172+
readonly domNode: HTMLElement;
172173
readonly onDidChangeViewModel: Event<void>;
173174
readonly onDidAcceptInput: Event<void>;
174175
readonly onDidHide: Event<void>;

src/vs/workbench/contrib/chat/browser/chatWidget.ts

+4
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ export class ChatWidget extends Disposable implements IChatWidget {
154154

155155
private listContainer!: HTMLElement;
156156
private container!: HTMLElement;
157+
get domNode() {
158+
return this.container;
159+
}
160+
157161
private welcomeMessageContainer!: HTMLElement;
158162
private readonly welcomePart: MutableDisposable<ChatViewWelcomePart> = this._register(new MutableDisposable());
159163

src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts

+5-17
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/i
88
import { IAction } from '../../../../../base/common/actions.js';
99
import { Event } from '../../../../../base/common/event.js';
1010
import { IDisposable } from '../../../../../base/common/lifecycle.js';
11-
import { localize } from '../../../../../nls.js';
1211
import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';
1312
import { MenuItemAction } from '../../../../../platform/actions/common/actions.js';
1413
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
1514
import { IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';
1615
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
1716
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
1817
import { IChatAgentService } from '../../common/chatAgents.js';
19-
import { ChatMode } from '../../common/constants.js';
18+
import { ChatMode, modeToString } from '../../common/constants.js';
19+
import { getOpenChatActionIdForMode } from '../actions/chatActions.js';
2020
import { IToggleChatModeArgs } from '../actions/chatExecuteActions.js';
2121

2222
export interface IModePickerDelegate {
@@ -35,8 +35,8 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
3535
) {
3636
const makeAction = (mode: ChatMode): IAction => ({
3737
...action,
38-
id: mode,
39-
label: this.modeToString(mode),
38+
id: getOpenChatActionIdForMode(mode),
39+
label: modeToString(mode),
4040
class: undefined,
4141
enabled: true,
4242
checked: delegate.getMode() === mode,
@@ -70,21 +70,9 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
7070
this._register(delegate.onDidChangeMode(() => this.renderLabel(this.element!)));
7171
}
7272

73-
private modeToString(mode: ChatMode) {
74-
switch (mode) {
75-
case ChatMode.Agent:
76-
return localize('chat.agentMode', "Agent");
77-
case ChatMode.Edit:
78-
return localize('chat.normalMode', "Edit");
79-
case ChatMode.Ask:
80-
default:
81-
return localize('chat.askMode', "Ask");
82-
}
83-
}
84-
8573
protected override renderLabel(element: HTMLElement): IDisposable | null {
8674
this.setAriaLabelAttributes(element);
87-
const state = this.modeToString(this.delegate.getMode());
75+
const state = modeToString(this.delegate.getMode());
8876
dom.reset(element, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`));
8977
return null;
9078
}

src/vs/workbench/contrib/chat/common/constants.ts

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ export enum ChatMode {
1616
Agent = 'agent'
1717
}
1818

19+
export function modeToString(mode: ChatMode) {
20+
switch (mode) {
21+
case ChatMode.Agent:
22+
return 'Agent';
23+
case ChatMode.Edit:
24+
return 'Edit';
25+
case ChatMode.Ask:
26+
default:
27+
return 'Ask';
28+
}
29+
}
30+
1931
export function validateChatMode(mode: unknown): ChatMode | undefined {
2032
switch (mode) {
2133
case ChatMode.Ask:

0 commit comments

Comments
 (0)