Skip to content

Commit e3d6829

Browse files
authored
new implicit context flow (microsoft#254768)
* new implicit flow * new implicit flow * some logic cleanup: * gate implicit context stuff behind setting * cleanup * hygiene weewoowewoo
1 parent 0900bb8 commit e3d6829

File tree

5 files changed

+129
-22
lines changed

5 files changed

+129
-22
lines changed

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

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from '../../../../../base/browser/dom.js';
7+
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
78
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
89
import { Button } from '../../../../../base/browser/ui/button/button.js';
910
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
1011
import { Codicon } from '../../../../../base/common/codicons.js';
12+
import { KeyCode } from '../../../../../base/common/keyCodes.js';
1113
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
1214
import { Schemas } from '../../../../../base/common/network.js';
1315
import { basename, dirname } from '../../../../../base/common/resources.js';
@@ -17,6 +19,7 @@ import { IModelService } from '../../../../../editor/common/services/model.js';
1719
import { localize } from '../../../../../nls.js';
1820
import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
1921
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
22+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
2023
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
2124
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
2225
import { FileKind, IFileService } from '../../../../../platform/files/common/files.js';
@@ -25,6 +28,8 @@ import { ILabelService } from '../../../../../platform/label/common/label.js';
2528
import { ResourceLabels } from '../../../../browser/labels.js';
2629
import { ResourceContextKey } from '../../../../common/contextkeys.js';
2730
import { IChatRequestImplicitVariableEntry } from '../../common/chatVariableEntries.js';
31+
import { IChatWidgetService } from '../chat.js';
32+
import { ChatAttachmentModel } from '../chatAttachmentModel.js';
2833

2934
export class ImplicitContextAttachmentWidget extends Disposable {
3035
public readonly domNode: HTMLElement;
@@ -34,6 +39,7 @@ export class ImplicitContextAttachmentWidget extends Disposable {
3439
constructor(
3540
private readonly attachment: IChatRequestImplicitVariableEntry,
3641
private readonly resourceLabels: ResourceLabels,
42+
private readonly attachmentModel: ChatAttachmentModel,
3743
@IContextKeyService private readonly contextKeyService: IContextKeyService,
3844
@IContextMenuService private readonly contextMenuService: IContextMenuService,
3945
@ILabelService private readonly labelService: ILabelService,
@@ -42,6 +48,8 @@ export class ImplicitContextAttachmentWidget extends Disposable {
4248
@ILanguageService private readonly languageService: ILanguageService,
4349
@IModelService private readonly modelService: IModelService,
4450
@IHoverService private readonly hoverService: IHoverService,
51+
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
52+
@IConfigurationService private readonly configService: IConfigurationService
4553
) {
4654
super();
4755

@@ -80,17 +88,56 @@ export class ImplicitContextAttachmentWidget extends Disposable {
8088
this.domNode.ariaLabel = ariaLabel;
8189
this.domNode.tabIndex = 0;
8290

83-
const hintLabel = localize('hint.label.current', "Current {0}", attachmentTypeName);
91+
const isSuggestedEnabled = this.configService.getValue('chat.implicitContext.suggestedContext');
92+
const hintLabel = !this.attachment.isSelection && !isSuggestedEnabled ? localize('hint.label.current', "Current {0}", attachmentTypeName) : '';
8493
const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, hintLabel));
8594
this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), hintElement, title));
8695

87-
const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName);
88-
const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg }));
89-
toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed;
90-
this.renderDisposables.add(toggleButton.onDidClick((e) => {
91-
e.stopPropagation(); // prevent it from triggering the click handler on the parent immediately after rerendering
92-
this.attachment.enabled = !this.attachment.enabled;
93-
}));
96+
97+
if (isSuggestedEnabled) {
98+
if (!this.attachment.isSelection) {
99+
const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName);
100+
const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg }));
101+
toggleButton.icon = this.attachment.enabled ? Codicon.x : Codicon.plus;
102+
this.renderDisposables.add(toggleButton.onDidClick((e) => {
103+
e.stopPropagation();
104+
e.preventDefault();
105+
if (!this.attachment.enabled) {
106+
this.convertToRegularAttachment();
107+
}
108+
this.attachment.enabled = false;
109+
}));
110+
}
111+
112+
if (!this.attachment.enabled && this.attachment.isSelection) {
113+
this.domNode.classList.remove('disabled');
114+
}
115+
116+
this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, e => {
117+
if (!this.attachment.enabled && !this.attachment.isSelection) {
118+
this.convertToRegularAttachment();
119+
}
120+
}));
121+
122+
this.renderDisposables.add(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, e => {
123+
const event = new StandardKeyboardEvent(e);
124+
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
125+
if (!this.attachment.enabled && !this.attachment.isSelection) {
126+
e.preventDefault();
127+
e.stopPropagation();
128+
this.convertToRegularAttachment();
129+
}
130+
}
131+
}));
132+
} else {
133+
const buttonMsg = this.attachment.enabled ? localize('disable', "Disable current {0} context", attachmentTypeName) : localize('enable', "Enable current {0} context", attachmentTypeName);
134+
const toggleButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: buttonMsg }));
135+
toggleButton.icon = this.attachment.enabled ? Codicon.eye : Codicon.eyeClosed;
136+
this.renderDisposables.add(toggleButton.onDidClick((e) => {
137+
e.stopPropagation(); // prevent it from triggering the click handler on the parent immediately after rerendering
138+
this.attachment.enabled = !this.attachment.enabled;
139+
}));
140+
}
94141

95142
// Context menu
96143
const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.domNode));
@@ -112,4 +159,13 @@ export class ImplicitContextAttachmentWidget extends Disposable {
112159
});
113160
}));
114161
}
162+
163+
private convertToRegularAttachment(): void {
164+
if (!this.attachment.value) {
165+
return;
166+
}
167+
const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value.uri;
168+
this.attachmentModel.addFile(file);
169+
this.chatWidgetService.lastFocusedWidget?.focusInput();
170+
}
115171
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ configurationRegistry.registerConfiguration({
170170
'panel': 'always',
171171
}
172172
},
173+
'chat.implicitContext.suggestedContext': {
174+
type: 'boolean',
175+
tags: ['experimental'],
176+
markdownDescription: nls.localize('chat.implicitContext.suggestedContext', "Controls whether the new implicit context flow is shown. In Ask and Edit modes, the context will automatically be included. In Agent mode context will be suggested as an attachment. Selections are always included as context."),
177+
default: true,
178+
},
173179
'chat.editing.autoAcceptDelay': {
174180
type: 'number',
175181
markdownDescription: nls.localize('chat.editing.autoAcceptDelay', "Delay after which changes made by chat are automatically accepted. Values are in seconds, `0` means disabled and `100` seconds is the maximum."),

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js';
102102
import { resizeImage } from './imageUtils.js';
103103
import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js';
104104
import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js';
105+
import { isEqual } from '../../../../base/common/resources.js';
106+
import { isLocation } from '../../../../editor/common/languages.js';
105107

106108
const $ = dom.$;
107109

@@ -169,7 +171,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
169171

170172
contextArr.add(...this.attachmentModel.attachments);
171173

172-
if (this.implicitContext?.enabled && this.implicitContext.value) {
174+
if ((this.implicitContext?.enabled && this.implicitContext?.value) || (isLocation(this.implicitContext?.value) && this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext'))) {
173175
const implicitChatVariables = this.implicitContext.toBaseEntries();
174176
contextArr.add(...implicitChatVariables);
175177
}
@@ -442,6 +444,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
442444
if (this._inputEditor) {
443445
this._inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() });
444446
}
447+
448+
if (this.implicitContext && this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext')) {
449+
this.implicitContext.enabled = this._currentModeObservable.get() !== ChatMode.Agent;
450+
}
445451
}));
446452
this._register(this._onDidChangeCurrentLanguageModel.event(() => {
447453
if (this._currentLanguageModel?.metadata.name) {
@@ -1301,8 +1307,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
13011307
this._indexOfLastOpenedContext = -1;
13021308
}
13031309

1304-
if (this.implicitContext?.value) {
1305-
const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels));
1310+
const isSuggestedEnabled = this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext');
1311+
1312+
if (this.implicitContext?.value && !isSuggestedEnabled) {
1313+
const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this.attachmentModel));
13061314
container.appendChild(implicitPart.domNode);
13071315
}
13081316

@@ -1355,6 +1363,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
13551363
}));
13561364
}
13571365

1366+
const implicitUri = this.implicitContext?.value;
1367+
const isUri = URI.isUri(implicitUri);
1368+
1369+
if (isSuggestedEnabled && implicitUri && (isUri || isLocation(implicitUri))) {
1370+
const targetUri = isUri ? implicitUri : implicitUri.uri;
1371+
const currentlyAttached = attachments.some(([, attachment]) => URI.isUri(attachment.value) && isEqual(attachment.value, targetUri));
1372+
1373+
const shouldShowImplicit = isUri ? !currentlyAttached : implicitUri.range;
1374+
if (shouldShowImplicit) {
1375+
const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this._attachmentModel));
1376+
container.appendChild(implicitPart.domNode);
1377+
}
1378+
}
13581379

13591380
if (oldHeight !== this.attachmentsContainer.offsetHeight) {
13601381
this._onDidChangeHeight.fire();
@@ -1369,9 +1390,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
13691390
this._indexOfLastAttachedContextDeletedWithKeyboard = index;
13701391
}
13711392

1372-
13731393
this._attachmentModel.delete(attachment.id);
13741394

1395+
1396+
if (this.configurationService.getValue<boolean>('chat.implicitContext.enableImplicitContext')) {
1397+
// if currently opened file is deleted, do not show implicit context
1398+
const implicitValue = URI.isUri(this.implicitContext?.value) && URI.isUri(attachment.value) && isEqual(this.implicitContext.value, attachment.value);
1399+
1400+
if (this.implicitContext?.isFile && implicitValue) {
1401+
this.implicitContext.enabled = false;
1402+
}
1403+
}
1404+
13751405
if (this._attachmentModel.size === 0) {
13761406
this.focus();
13771407
}

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -161,17 +161,21 @@ export class ChatImplicitContextContribution extends Disposable implements IWork
161161
newValue = { uri: model.uri, range: selection } satisfies Location;
162162
isSelection = true;
163163
} else {
164-
const visibleRanges = codeEditor?.getVisibleRanges();
165-
if (visibleRanges && visibleRanges.length > 0) {
166-
// Merge visible ranges. Maybe the reference value could actually be an array of Locations?
167-
// Something like a Location with an array of Ranges?
168-
let range = visibleRanges[0];
169-
visibleRanges.slice(1).forEach(r => {
170-
range = range.plusRange(r);
171-
});
172-
newValue = { uri: model.uri, range } satisfies Location;
173-
} else {
164+
if (this.configurationService.getValue('chat.implicitContext.suggestedContext')) {
174165
newValue = model.uri;
166+
} else {
167+
const visibleRanges = codeEditor?.getVisibleRanges();
168+
if (visibleRanges && visibleRanges.length > 0) {
169+
// Merge visible ranges. Maybe the reference value could actually be an array of Locations?
170+
// Something like a Location with an array of Ranges?
171+
let range = visibleRanges[0];
172+
visibleRanges.slice(1).forEach(r => {
173+
range = range.plusRange(r);
174+
});
175+
newValue = { uri: model.uri, range } satisfies Location;
176+
} else {
177+
newValue = model.uri;
178+
}
175179
}
176180
}
177181
}

src/vs/workbench/contrib/chat/browser/media/chat.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,10 @@ have to be updated for changes to the rules above, or to support more deeply nes
12551255
outline-offset: -4px;
12561256
}
12571257

1258+
.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-plus {
1259+
font-size: 12px;
1260+
}
1261+
12581262
.chat-related-files .monaco-button.codicon.codicon-add:hover,
12591263
.action-item.chat-attached-context-attachment.chat-add-files:hover,
12601264
.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover {
@@ -2061,3 +2065,10 @@ have to be updated for changes to the rules above, or to support more deeply nes
20612065
width: 1px;
20622066
height: 1px;
20632067
}
2068+
2069+
2070+
.interactive-session .chat-attached-context .chat-attached-context-attachment.implicit.disabled:hover {
2071+
cursor: pointer;
2072+
border-style: solid;
2073+
background-color: var(--vscode-toolbar-hoverBackground);
2074+
}

0 commit comments

Comments
 (0)