Merge branch 'main' into aiday/indentationWithinCommentsTest

This commit is contained in:
Aiday Marlen Kyzy 2024-03-21 09:39:04 +01:00 committed by GitHub
commit 1403f05edd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 1027 additions and 392 deletions

View file

@ -38,14 +38,14 @@
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.", "configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.",
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.", "configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.",
"configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.", "configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.",
"configuration.markdown.editor.drop.always": "Always insert Markdown links.", "configuration.markdown.editor.drop.enabled.always": "Always insert Markdown links.",
"configuration.markdown.editor.drop.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.", "configuration.markdown.editor.drop.enabled.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.drop.never": "Never create Markdown links.", "configuration.markdown.editor.drop.enabled.never": "Never create Markdown links.",
"configuration.markdown.editor.drop.copyIntoWorkspace": "Controls if files outside of the workspace that are dropped into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied dropped files should be created", "configuration.markdown.editor.drop.copyIntoWorkspace": "Controls if files outside of the workspace that are dropped into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied dropped files should be created",
"configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.", "configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.",
"configuration.markdown.editor.filePaste.always": "Always insert Markdown links.", "configuration.markdown.editor.filePaste.enabled.always": "Always insert Markdown links.",
"configuration.markdown.editor.filePaste.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.", "configuration.markdown.editor.filePaste.enabled.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.filePaste.never": "Never create Markdown links.", "configuration.markdown.editor.filePaste.enabled.never": "Never create Markdown links.",
"configuration.markdown.editor.filePaste.copyIntoWorkspace": "Controls if files outside of the workspace that are pasted into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied files should be created.", "configuration.markdown.editor.filePaste.copyIntoWorkspace": "Controls if files outside of the workspace that are pasted into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied files should be created.",
"configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.", "configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.",
"configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.", "configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.",

View file

@ -65,6 +65,7 @@
"contributes": { "contributes": {
"chatParticipants": [ "chatParticipants": [
{ {
"id": "api-test.participant",
"name": "participant", "name": "participant",
"description": "test", "description": "test",
"isDefault": true, "isDefault": true,

View file

@ -30,7 +30,7 @@ suite('chat', () => {
function setupParticipant(): Event<{ request: ChatRequest; context: ChatContext }> { function setupParticipant(): Event<{ request: ChatRequest; context: ChatContext }> {
const emitter = new EventEmitter<{ request: ChatRequest; context: ChatContext }>(); const emitter = new EventEmitter<{ request: ChatRequest; context: ChatContext }>();
disposables.push(); disposables.push(emitter);
disposables.push(interactive.registerInteractiveSessionProvider('provider', { disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_token: CancellationToken): ProviderResult<InteractiveSession> => { prepareSession: (_token: CancellationToken): ProviderResult<InteractiveSession> => {
return { return {
@ -40,7 +40,7 @@ suite('chat', () => {
}, },
})); }));
const participant = chat.createChatParticipant('participant', (request, context, _progress, _token) => { const participant = chat.createChatParticipant('api-test.participant', (request, context, _progress, _token) => {
emitter.fire({ request, context }); emitter.fire({ request, context });
return null; return null;
}); });
@ -49,12 +49,12 @@ suite('chat', () => {
return emitter.event; return emitter.event;
} }
test('participant and slash command', async () => { test('participant and slash command history', async () => {
const onRequest = setupParticipant(); const onRequest = setupParticipant();
commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' });
let i = 0; let i = 0;
onRequest(request => { disposables.push(onRequest(request => {
if (i === 0) { if (i === 0) {
assert.deepStrictEqual(request.request.command, 'hello'); assert.deepStrictEqual(request.request.command, 'hello');
assert.strictEqual(request.request.prompt, 'friend'); assert.strictEqual(request.request.prompt, 'friend');
@ -62,10 +62,10 @@ suite('chat', () => {
commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' });
} else { } else {
assert.strictEqual(request.context.history.length, 1); assert.strictEqual(request.context.history.length, 1);
assert.strictEqual(request.context.history[0].participant.name, 'participant'); assert.strictEqual(request.context.history[0].participant, 'api-test.participant');
assert.strictEqual(request.context.history[0].command, 'hello'); assert.strictEqual(request.context.history[0].command, 'hello');
} }
}); }));
}); });
test('participant and variable', async () => { test('participant and variable', async () => {
@ -93,7 +93,7 @@ suite('chat', () => {
})); }));
const deferred = new DeferredPromise<ChatResult>(); const deferred = new DeferredPromise<ChatResult>();
const participant = chat.createChatParticipant('participant', (_request, _context, _progress, _token) => { const participant = chat.createChatParticipant('api-test.participant', (_request, _context, _progress, _token) => {
return { metadata: { key: 'value' } }; return { metadata: { key: 'value' } };
}); });
participant.isDefault = true; participant.isDefault = true;

View file

@ -1,7 +1,7 @@
{ {
"name": "code-oss-dev", "name": "code-oss-dev",
"version": "1.88.0", "version": "1.88.0",
"distro": "ff3bff60edcc6e1f7269509e1673036c00fa62bd", "distro": "7734ec27c8ec09ddc68bc3618e17d9a8b40fbfd9",
"author": { "author": {
"name": "Microsoft Corporation" "name": "Microsoft Corporation"
}, },

View file

@ -312,6 +312,18 @@ export class InputBox extends Widget {
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd; return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
} }
public getSelection(): IRange | null {
const selectionStart = this.input.selectionStart;
if (selectionStart === null) {
return null;
}
const selectionEnd = this.input.selectionEnd ?? selectionStart;
return {
start: selectionStart,
end: selectionEnd,
};
}
public enable(): void { public enable(): void {
this.input.removeAttribute('disabled'); this.input.removeAttribute('disabled');
} }

View file

@ -237,7 +237,7 @@ export class Checkbox extends Widget {
constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) { constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) {
super(); super();
this.checkbox = new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles }); this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles }));
this.domNode = this.checkbox.domNode; this.domNode = this.checkbox.domNode;

View file

@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { Position } from 'vs/editor/common/core/position'; import { Position } from 'vs/editor/common/core/position';
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
import { TokenizationRegistry } from 'vs/editor/common/languages'; import { TokenizationRegistry } from 'vs/editor/common/languages';
import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground } from 'vs/editor/common/core/editorColorRegistry'; import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground, editorMultiCursorSecondaryForeground, editorMultiCursorPrimaryForeground } from 'vs/editor/common/core/editorColorRegistry';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import { EditorTheme } from 'vs/editor/common/editorTheme'; import { EditorTheme } from 'vs/editor/common/editorTheme';
@ -29,7 +29,9 @@ class Settings {
public readonly borderColor: string | null; public readonly borderColor: string | null;
public readonly hideCursor: boolean; public readonly hideCursor: boolean;
public readonly cursorColor: string | null; public readonly cursorColorSingle: string | null;
public readonly cursorColorPrimary: string | null;
public readonly cursorColorSecondary: string | null;
public readonly themeType: 'light' | 'dark' | 'hcLight' | 'hcDark'; public readonly themeType: 'light' | 'dark' | 'hcLight' | 'hcDark';
public readonly backgroundColor: Color | null; public readonly backgroundColor: Color | null;
@ -55,8 +57,12 @@ class Settings {
this.borderColor = borderColor ? borderColor.toString() : null; this.borderColor = borderColor ? borderColor.toString() : null;
this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler); this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler);
const cursorColor = theme.getColor(editorCursorForeground); const cursorColorSingle = theme.getColor(editorCursorForeground);
this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; this.cursorColorSingle = cursorColorSingle ? cursorColorSingle.transparent(0.7).toString() : null;
const cursorColorPrimary = theme.getColor(editorMultiCursorPrimaryForeground);
this.cursorColorPrimary = cursorColorPrimary ? cursorColorPrimary.transparent(0.7).toString() : null;
const cursorColorSecondary = theme.getColor(editorMultiCursorSecondaryForeground);
this.cursorColorSecondary = cursorColorSecondary ? cursorColorSecondary.transparent(0.7).toString() : null;
this.themeType = theme.type; this.themeType = theme.type;
@ -189,7 +195,9 @@ class Settings {
&& this.renderBorder === other.renderBorder && this.renderBorder === other.renderBorder
&& this.borderColor === other.borderColor && this.borderColor === other.borderColor
&& this.hideCursor === other.hideCursor && this.hideCursor === other.hideCursor
&& this.cursorColor === other.cursorColor && this.cursorColorSingle === other.cursorColorSingle
&& this.cursorColorPrimary === other.cursorColorPrimary
&& this.cursorColorSecondary === other.cursorColorSecondary
&& this.themeType === other.themeType && this.themeType === other.themeType
&& Color.equals(this.backgroundColor, other.backgroundColor) && Color.equals(this.backgroundColor, other.backgroundColor)
&& this.top === other.top && this.top === other.top
@ -213,6 +221,11 @@ const enum OverviewRulerLane {
Full = 7 Full = 7
} }
type Cursor = {
position: Position;
color: string | null;
};
const enum ShouldRenderValue { const enum ShouldRenderValue {
NotNeeded = 0, NotNeeded = 0,
Maybe = 1, Maybe = 1,
@ -226,10 +239,10 @@ export class DecorationsOverviewRuler extends ViewPart {
private readonly _tokensColorTrackerListener: IDisposable; private readonly _tokensColorTrackerListener: IDisposable;
private readonly _domNode: FastDomNode<HTMLCanvasElement>; private readonly _domNode: FastDomNode<HTMLCanvasElement>;
private _settings!: Settings; private _settings!: Settings;
private _cursorPositions: Position[]; private _cursorPositions: Cursor[];
private _renderedDecorations: OverviewRulerDecorationsGroup[] = []; private _renderedDecorations: OverviewRulerDecorationsGroup[] = [];
private _renderedCursorPositions: Position[] = []; private _renderedCursorPositions: Cursor[] = [];
constructor(context: ViewContext) { constructor(context: ViewContext) {
super(context); super(context);
@ -249,7 +262,7 @@ export class DecorationsOverviewRuler extends ViewPart {
} }
}); });
this._cursorPositions = [new Position(1, 1)]; this._cursorPositions = [{ position: new Position(1, 1), color: this._settings.cursorColorSingle }];
} }
public override dispose(): void { public override dispose(): void {
@ -298,9 +311,13 @@ export class DecorationsOverviewRuler extends ViewPart {
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._cursorPositions = []; this._cursorPositions = [];
for (let i = 0, len = e.selections.length; i < len; i++) { for (let i = 0, len = e.selections.length; i < len; i++) {
this._cursorPositions[i] = e.selections[i].getPosition(); let color = this._settings.cursorColorSingle;
if (len > 1) {
color = i === 0 ? this._settings.cursorColorPrimary : this._settings.cursorColorSecondary;
}
this._cursorPositions.push({ position: e.selections[i].getPosition(), color });
} }
this._cursorPositions.sort(Position.compare); this._cursorPositions.sort((a, b) => Position.compare(a.position, b.position));
return this._markRenderingIsMaybeNeeded(); return this._markRenderingIsMaybeNeeded();
} }
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
@ -352,7 +369,7 @@ export class DecorationsOverviewRuler extends ViewPart {
if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) { if (this._actualShouldRender === ShouldRenderValue.Maybe && !OverviewRulerDecorationsGroup.equalsArr(this._renderedDecorations, decorations)) {
this._actualShouldRender = ShouldRenderValue.Needed; this._actualShouldRender = ShouldRenderValue.Needed;
} }
if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.lineNumber === b.lineNumber)) { if (this._actualShouldRender === ShouldRenderValue.Maybe && !equals(this._renderedCursorPositions, this._cursorPositions, (a, b) => a.position.lineNumber === b.position.lineNumber && a.color === b.color)) {
this._actualShouldRender = ShouldRenderValue.Needed; this._actualShouldRender = ShouldRenderValue.Needed;
} }
if (this._actualShouldRender === ShouldRenderValue.Maybe) { if (this._actualShouldRender === ShouldRenderValue.Maybe) {
@ -443,17 +460,21 @@ export class DecorationsOverviewRuler extends ViewPart {
} }
// Draw cursors // Draw cursors
if (!this._settings.hideCursor && this._settings.cursorColor) { if (!this._settings.hideCursor) {
const cursorHeight = (2 * this._settings.pixelRatio) | 0; const cursorHeight = (2 * this._settings.pixelRatio) | 0;
const halfCursorHeight = (cursorHeight / 2) | 0; const halfCursorHeight = (cursorHeight / 2) | 0;
const cursorX = this._settings.x[OverviewRulerLane.Full]; const cursorX = this._settings.x[OverviewRulerLane.Full];
const cursorW = this._settings.w[OverviewRulerLane.Full]; const cursorW = this._settings.w[OverviewRulerLane.Full];
canvasCtx.fillStyle = this._settings.cursorColor;
let prevY1 = -100; let prevY1 = -100;
let prevY2 = -100; let prevY2 = -100;
let prevColor: string | null = null;
for (let i = 0, len = this._cursorPositions.length; i < len; i++) { for (let i = 0, len = this._cursorPositions.length; i < len; i++) {
const cursor = this._cursorPositions[i]; const color = this._cursorPositions[i].color;
if (!color) {
continue;
}
const cursor = this._cursorPositions[i].position;
let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0; let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0;
if (yCenter < halfCursorHeight) { if (yCenter < halfCursorHeight) {
@ -464,9 +485,9 @@ export class DecorationsOverviewRuler extends ViewPart {
const y1 = yCenter - halfCursorHeight; const y1 = yCenter - halfCursorHeight;
const y2 = y1 + cursorHeight; const y2 = y1 + cursorHeight;
if (y1 > prevY2 + 1) { if (y1 > prevY2 + 1 || color !== prevColor) {
// flush prev // flush prev
if (i !== 0) { if (i !== 0 && prevColor) {
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1); canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
} }
prevY1 = y1; prevY1 = y1;
@ -477,8 +498,12 @@ export class DecorationsOverviewRuler extends ViewPart {
prevY2 = y2; prevY2 = y2;
} }
} }
prevColor = color;
canvasCtx.fillStyle = color;
}
if (prevColor) {
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
} }
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
} }
if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) { if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) {

View file

@ -35,6 +35,12 @@ class ViewCursorRenderData {
) { } ) { }
} }
export enum CursorPlurality {
Single,
MultiPrimary,
MultiSecondary,
}
export class ViewCursor { export class ViewCursor {
private readonly _context: ViewContext; private readonly _context: ViewContext;
private readonly _domNode: FastDomNode<HTMLElement>; private readonly _domNode: FastDomNode<HTMLElement>;
@ -47,11 +53,12 @@ export class ViewCursor {
private _isVisible: boolean; private _isVisible: boolean;
private _position: Position; private _position: Position;
private _pluralityClass: string;
private _lastRenderedContent: string; private _lastRenderedContent: string;
private _renderData: ViewCursorRenderData | null; private _renderData: ViewCursorRenderData | null;
constructor(context: ViewContext) { constructor(context: ViewContext, plurality: CursorPlurality) {
this._context = context; this._context = context;
const options = this._context.configuration.options; const options = this._context.configuration.options;
const fontInfo = options.get(EditorOption.fontInfo); const fontInfo = options.get(EditorOption.fontInfo);
@ -73,6 +80,8 @@ export class ViewCursor {
this._domNode.setDisplay('none'); this._domNode.setDisplay('none');
this._position = new Position(1, 1); this._position = new Position(1, 1);
this._pluralityClass = '';
this.setPlurality(plurality);
this._lastRenderedContent = ''; this._lastRenderedContent = '';
this._renderData = null; this._renderData = null;
@ -86,6 +95,23 @@ export class ViewCursor {
return this._position; return this._position;
} }
public setPlurality(plurality: CursorPlurality) {
switch (plurality) {
default:
case CursorPlurality.Single:
this._pluralityClass = '';
break;
case CursorPlurality.MultiPrimary:
this._pluralityClass = 'cursor-primary';
break;
case CursorPlurality.MultiSecondary:
this._pluralityClass = 'cursor-secondary';
break;
}
}
public show(): void { public show(): void {
if (!this._isVisible) { if (!this._isVisible) {
this._domNode.setVisibility('inherit'); this._domNode.setVisibility('inherit');
@ -229,7 +255,7 @@ export class ViewCursor {
this._domNode.domNode.textContent = this._lastRenderedContent; this._domNode.domNode.textContent = this._lastRenderedContent;
} }
this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`); this._domNode.setClassName(`cursor ${this._pluralityClass} ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`);
this._domNode.setDisplay('block'); this._domNode.setDisplay('block');
this._domNode.setTop(this._renderData.top); this._domNode.setTop(this._renderData.top);

View file

@ -7,10 +7,14 @@ import 'vs/css!./viewCursors';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async'; import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { IViewCursorRenderData, ViewCursor, CursorPlurality } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from 'vs/editor/common/config/editorOptions'; import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position'; import { Position } from 'vs/editor/common/core/position';
import { editorCursorBackground, editorCursorForeground } from 'vs/editor/common/core/editorColorRegistry'; import {
editorCursorBackground, editorCursorForeground,
editorMultiCursorPrimaryForeground, editorMultiCursorPrimaryBackground,
editorMultiCursorSecondaryForeground, editorMultiCursorSecondaryBackground
} from 'vs/editor/common/core/editorColorRegistry';
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import * as viewEvents from 'vs/editor/common/viewEvents'; import * as viewEvents from 'vs/editor/common/viewEvents';
@ -57,7 +61,7 @@ export class ViewCursors extends ViewPart {
this._isVisible = false; this._isVisible = false;
this._primaryCursor = new ViewCursor(this._context); this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single);
this._secondaryCursors = []; this._secondaryCursors = [];
this._renderData = []; this._renderData = [];
@ -88,6 +92,7 @@ export class ViewCursors extends ViewPart {
} }
// --- begin event handlers // --- begin event handlers
public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean { public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean {
this._isComposingInput = true; this._isComposingInput = true;
this._updateBlinking(); this._updateBlinking();
@ -120,6 +125,7 @@ export class ViewCursors extends ViewPart {
this._secondaryCursors.length !== secondaryPositions.length this._secondaryCursors.length !== secondaryPositions.length
|| (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit) || (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit)
); );
this._primaryCursor.setPlurality(secondaryPositions.length ? CursorPlurality.MultiPrimary : CursorPlurality.Single);
this._primaryCursor.onCursorPositionChanged(position, pauseAnimation); this._primaryCursor.onCursorPositionChanged(position, pauseAnimation);
this._updateBlinking(); this._updateBlinking();
@ -127,7 +133,7 @@ export class ViewCursors extends ViewPart {
// Create new cursors // Create new cursors
const addCnt = secondaryPositions.length - this._secondaryCursors.length; const addCnt = secondaryPositions.length - this._secondaryCursors.length;
for (let i = 0; i < addCnt; i++) { for (let i = 0; i < addCnt; i++) {
const newCursor = new ViewCursor(this._context); const newCursor = new ViewCursor(this._context, CursorPlurality.MultiSecondary);
this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling); this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling);
this._secondaryCursors.push(newCursor); this._secondaryCursors.push(newCursor);
} }
@ -160,7 +166,6 @@ export class ViewCursors extends ViewPart {
return true; return true;
} }
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text // true for inline decorations that can end up relayouting text
return true; return true;
@ -263,6 +268,7 @@ export class ViewCursors extends ViewPart {
} }
} }
} }
// --- end blinking logic // --- end blinking logic
private _updateDomClassName(): void { private _updateDomClassName(): void {
@ -375,16 +381,29 @@ export class ViewCursors extends ViewPart {
} }
registerThemingParticipant((theme, collector) => { registerThemingParticipant((theme, collector) => {
const caret = theme.getColor(editorCursorForeground); type CursorTheme = {
if (caret) { foreground: string;
let caretBackground = theme.getColor(editorCursorBackground); background: string;
if (!caretBackground) { class: string;
caretBackground = caret.opposite(); };
}
collector.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`); const cursorThemes: CursorTheme[] = [
if (isHighContrast(theme.type)) { { class: '.cursor', foreground: editorCursorForeground, background: editorCursorBackground },
collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`); { class: '.cursor-primary', foreground: editorMultiCursorPrimaryForeground, background: editorMultiCursorPrimaryBackground },
{ class: '.cursor-secondary', foreground: editorMultiCursorSecondaryForeground, background: editorMultiCursorSecondaryBackground },
];
for (const cursorTheme of cursorThemes) {
const caret = theme.getColor(cursorTheme.foreground);
if (caret) {
let caretBackground = theme.getColor(cursorTheme.background);
if (!caretBackground) {
caretBackground = caret.opposite();
}
collector.addRule(`.monaco-editor .cursors-layer ${cursorTheme.class} { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
if (isHighContrast(theme.type)) {
collector.addRule(`.monaco-editor .cursors-layer.has-selection ${cursorTheme.class} { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
}
} }
} }
}); });

View file

@ -20,6 +20,10 @@ export const editorSymbolHighlightBorder = registerColor('editor.symbolHighlight
export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('caret', 'Color of the editor cursor.')); export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('caret', 'Color of the editor cursor.'));
export const editorCursorBackground = registerColor('editorCursor.background', null, nls.localize('editorCursorBackground', 'The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.')); export const editorCursorBackground = registerColor('editorCursor.background', null, nls.localize('editorCursorBackground', 'The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.'));
export const editorMultiCursorPrimaryForeground = registerColor('editorMultiCursor.primary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorPrimaryForeground', 'Color of the primary editor cursor when multiple cursors are present.'));
export const editorMultiCursorPrimaryBackground = registerColor('editorMultiCursor.primary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorPrimaryBackground', 'The background color of the primary editor cursor when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.'));
export const editorMultiCursorSecondaryForeground = registerColor('editorMultiCursor.secondary.foreground', { dark: editorCursorForeground, light: editorCursorForeground, hcDark: editorCursorForeground, hcLight: editorCursorForeground }, nls.localize('editorMultiCursorSecondaryForeground', 'Color of secondary editor cursors when multiple cursors are present.'));
export const editorMultiCursorSecondaryBackground = registerColor('editorMultiCursor.secondary.background', { dark: editorCursorBackground, light: editorCursorBackground, hcDark: editorCursorBackground, hcLight: editorCursorBackground }, nls.localize('editorMultiCursorSecondaryBackground', 'The background color of secondary editor cursors when multiple cursors are present. Allows customizing the color of a character overlapped by a block cursor.'));
export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hcDark: '#e3e4e229', hcLight: '#CCCCCC' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.')); export const editorWhitespaces = registerColor('editorWhitespace.foreground', { dark: '#e3e4e229', light: '#33333333', hcDark: '#e3e4e229', hcLight: '#CCCCCC' }, nls.localize('editorWhitespaces', 'Color of whitespace characters in the editor.'));
export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hcDark: Color.white, hcLight: '#292929' }, nls.localize('editorLineNumbers', 'Color of editor line numbers.')); export const editorLineNumbers = registerColor('editorLineNumber.foreground', { dark: '#858585', light: '#237893', hcDark: Color.white, hcLight: '#292929' }, nls.localize('editorLineNumbers', 'Color of editor line numbers.'));

View file

@ -36,8 +36,9 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress';
import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; import { editorFindMatchHighlight, editorFindMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry';
import { isHighContrast } from 'vs/platform/theme/common/theme'; import { isHighContrast } from 'vs/platform/theme/common/theme';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types'; import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
import { CodeActionModel, CodeActionsState } from './codeActionModel'; import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel';
import { HierarchicalKind } from 'vs/base/common/hierarchicalKind';
interface IActionShowOptions { interface IActionShowOptions {
@ -291,7 +292,22 @@ export class CodeActionController extends Disposable implements IEditorContribut
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return; return;
} }
return { canPreview: !!action.action.edit?.edits.length };
let canPreview = false;
const actionKind = action.action.kind;
if (actionKind) {
const hierarchicalKind = new HierarchicalKind(actionKind);
const refactorKinds = [
CodeActionKind.RefactorExtract,
CodeActionKind.RefactorInline,
CodeActionKind.RefactorRewrite
];
canPreview = refactorKinds.some(refactorKind => refactorKind.contains(hierarchicalKind));
}
return { canPreview: canPreview || !!action.action.edit?.edits.length };
}, },
onFocus: (action: CodeActionItem | undefined) => { onFocus: (action: CodeActionItem | undefined) => {
if (action && action.action) { if (action && action.action) {

View file

@ -545,9 +545,10 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => {
// Failing tests from issues... // Failing tests from issues...
test.skip('issue #116843: indent after arrow function', () => { test.skip('issue #208215: indent after arrow function', () => {
// https://github.com/microsoft/vscode/issues/116843 // https://github.com/microsoft/vscode/issues/208215
// consider the regex: /^\s*(var|const|let)\s+\w+\s*=\s*\(.*\)\s*=>\s*$/
const model = createTextModel("", languageId, {}); const model = createTextModel("", languageId, {});
disposables.add(model); disposables.add(model);
@ -562,11 +563,53 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => {
'const add1 = (n) =>', 'const add1 = (n) =>',
' ', ' ',
].join('\n')); ].join('\n'));
viewModel.type('n + 1;'); });
});
test.skip('issue #208215: outdented after semicolon detected after arrow function', () => {
// Notes: we want to outdent after having detected a semi-colon which marks the end of the line, but only when we have detected an arrow function
// We could use one outdent pattern corresponding per indent pattern, and not a generic outdent and indent pattern
const model = createTextModel([
'const add1 = (n) =>',
' console.log("hi");',
].join('\n'), languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
editor.setSelection(new Selection(2, 24, 2, 24));
viewModel.type("\n", 'keyboard'); viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [ assert.strictEqual(model.getValue(), [
'const add1 = (n) =>', 'const add1 = (n) =>',
' n + 1;', ' console.log("hi");',
'',
].join('\n'));
});
});
test.skip('issue #116843: indent after arrow function', () => {
// https://github.com/microsoft/vscode/issues/116843
const model = createTextModel("", languageId, {});
disposables.add(model);
withTestCodeEditor(model, { autoIndent: "full" }, (editor, viewModel, instantiationService) => {
registerLanguage(instantiationService, languageId, Language.TypeScript, disposables);
viewModel.type([
'const add1 = (n) =>',
' n + 1;',
].join('\n'));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'const add1 = (n) =>',
' n + 1;',
'', '',
].join('\n')); ].join('\n'));
}); });
@ -632,6 +675,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => {
test.skip('issue #43244: incorrect indentation', () => { test.skip('issue #43244: incorrect indentation', () => {
// https://github.com/microsoft/vscode/issues/43244 // https://github.com/microsoft/vscode/issues/43244
// potential regex to fix: "^.*[if|while|for]\s*\(.*\)\s*",
const model = createTextModel([ const model = createTextModel([
'function f() {', 'function f() {',

View file

@ -237,9 +237,9 @@ export class LinkDetector extends Disposable implements IEditorContribution {
const fsPath = resources.originalFSPath(parsedUri); const fsPath = resources.originalFSPath(parsedUri);
let relativePath: string | null = null; let relativePath: string | null = null;
if (fsPath.startsWith('/./')) { if (fsPath.startsWith('/./') || fsPath.startsWith('\\.\\')) {
relativePath = `.${fsPath.substr(1)}`; relativePath = `.${fsPath.substr(1)}`;
} else if (fsPath.startsWith('//./')) { } else if (fsPath.startsWith('//./') || fsPath.startsWith('\\\\.\\')) {
relativePath = `.${fsPath.substr(2)}`; relativePath = `.${fsPath.substr(2)}`;
} }

View file

@ -38,7 +38,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IEditorProgressService } from 'vs/platform/progress/common/progress';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameInputField, RenameInputFieldResult } from './renameInputField'; import { CONTEXT_RENAME_INPUT_VISIBLE, NewNameSource, RenameInputFieldResult, RenameWidget } from './renameInputField';
class RenameSkeleton { class RenameSkeleton {
@ -138,7 +138,7 @@ class RenameController implements IEditorContribution {
return editor.getContribution<RenameController>(RenameController.ID); return editor.getContribution<RenameController>(RenameController.ID);
} }
private readonly _renameInputField: RenameInputField; private readonly _renameInputField: RenameWidget;
private readonly _disposableStore = new DisposableStore(); private readonly _disposableStore = new DisposableStore();
private _cts: CancellationTokenSource = new CancellationTokenSource(); private _cts: CancellationTokenSource = new CancellationTokenSource();
@ -153,7 +153,7 @@ class RenameController implements IEditorContribution {
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ITelemetryService private readonly _telemetryService: ITelemetryService, @ITelemetryService private readonly _telemetryService: ITelemetryService,
) { ) {
this._renameInputField = this._disposableStore.add(this._instaService.createInstance(RenameInputField, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])); this._renameInputField = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview']));
} }
dispose(): void { dispose(): void {
@ -229,23 +229,19 @@ class RenameController implements IEditorContribution {
const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled
const renameCandidatesCts = new CancellationTokenSource(cts2.token);
const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model); const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model);
const newSymbolNameProvidersResults = newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, renameCandidatesCts.token));
trace(`requested new symbol names from ${newSymbolNamesProviders.length} providers`);
const selection = this.editor.getSelection(); const requestRenameSuggestions = (cts: CancellationToken) => newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, cts));
let selectionStart = 0;
let selectionEnd = loc.text.length;
if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(loc.range, selection)) {
selectionStart = Math.max(0, selection.startColumn - loc.range.startColumn);
selectionEnd = Math.min(loc.range.endColumn, selection.endColumn) - loc.range.startColumn;
}
trace('creating rename input field and awaiting its result'); trace('creating rename input field and awaiting its result');
const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue<boolean>(this.editor.getModel().uri, 'editor.rename.enablePreview'); const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue<boolean>(this.editor.getModel().uri, 'editor.rename.enablePreview');
const inputFieldResult = await this._renameInputField.getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, newSymbolNameProvidersResults, renameCandidatesCts); const inputFieldResult = await this._renameInputField.getInput(
loc.range,
loc.text,
supportPreview,
requestRenameSuggestions,
cts2
);
trace('received response from rename input field'); trace('received response from rename input field');
if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently

View file

@ -23,7 +23,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { IDimension } from 'vs/editor/common/core/dimension'; import { IDimension } from 'vs/editor/common/core/dimension';
import { Position } from 'vs/editor/common/core/position'; import { Position } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range'; import { IRange, Range } from 'vs/editor/common/core/range';
import { ScrollType } from 'vs/editor/common/editorCommon'; import { ScrollType } from 'vs/editor/common/editorCommon';
import { NewSymbolName, NewSymbolNameTag, ProviderResult } from 'vs/editor/common/languages'; import { NewSymbolName, NewSymbolNameTag, ProviderResult } from 'vs/editor/common/languages';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
@ -85,7 +85,13 @@ interface IRenameInputField {
/** /**
* @returns a `boolean` standing for `shouldFocusEditor`, if user didn't pick a new name, or a {@link RenameInputFieldResult} * @returns a `boolean` standing for `shouldFocusEditor`, if user didn't pick a new name, or a {@link RenameInputFieldResult}
*/ */
getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult<NewSymbolName[]>[], cts: CancellationTokenSource): Promise<RenameInputFieldResult | boolean>; getInput(
where: IRange,
currentName: string,
supportPreview: boolean,
requestRenameSuggestions: (cts: CancellationToken) => ProviderResult<NewSymbolName[]>[],
cts: CancellationTokenSource
): Promise<RenameInputFieldResult | boolean>;
acceptInput(wantsPreview: boolean): void; acceptInput(wantsPreview: boolean): void;
cancelInput(focusEditor: boolean, caller: string): void; cancelInput(focusEditor: boolean, caller: string): void;
@ -94,7 +100,7 @@ interface IRenameInputField {
focusPreviousRenameSuggestion(): void; focusPreviousRenameSuggestion(): void;
} }
export class RenameInputField implements IRenameInputField, IContentWidget, IDisposable { export class RenameWidget implements IRenameInputField, IContentWidget, IDisposable {
// implement IContentWidget // implement IContentWidget
readonly allowEditorOverflow: boolean = true; readonly allowEditorOverflow: boolean = true;
@ -127,6 +133,8 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
*/ */
private _timeBeforeFirstInputFieldEdit: number | undefined; private _timeBeforeFirstInputFieldEdit: number | undefined;
private _renameCandidateProvidersCts: CancellationTokenSource | undefined;
private readonly _visibleContextKey: IContextKey<boolean>; private readonly _visibleContextKey: IContextKey<boolean>;
private readonly _disposables = new DisposableStore(); private readonly _disposables = new DisposableStore();
@ -194,6 +202,9 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
this._isEditingRenameCandidate = true; this._isEditingRenameCandidate = true;
} }
this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed(); this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed();
if (this._renameCandidateProvidersCts?.token.isCancellationRequested === false) {
this._renameCandidateProvidersCts.cancel();
}
this._renameCandidateListView?.clearFocus(); this._renameCandidateListView?.clearFocus();
}) })
); );
@ -346,7 +357,19 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
} }
} }
getInput(where: IRange, currentName: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, candidates: ProviderResult<NewSymbolName[]>[], cts: CancellationTokenSource): Promise<RenameInputFieldResult | boolean> { getInput(
where: IRange,
currentName: string,
supportPreview: boolean,
requestRenameSuggestions: (cts: CancellationToken) => ProviderResult<NewSymbolName[]>[],
cts: CancellationTokenSource
): Promise<RenameInputFieldResult | boolean> {
const { start: selectionStart, end: selectionEnd } = this._getSelection(where, currentName);
this._renameCandidateProvidersCts = new CancellationTokenSource();
const candidates = requestRenameSuggestions(this._renameCandidateProvidersCts.token);
this._updateRenameCandidates(candidates, currentName, cts.token);
this._isEditingRenameCandidate = false; this._isEditingRenameCandidate = false;
@ -365,8 +388,12 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
const disposeOnDone = new DisposableStore(); const disposeOnDone = new DisposableStore();
disposeOnDone.add(toDisposable(() => cts.dispose(true))); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call disposeOnDone.add(toDisposable(() => cts.dispose(true))); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call
disposeOnDone.add(toDisposable(() => {
this._updateRenameCandidates(candidates, currentName, cts.token); if (this._renameCandidateProvidersCts !== undefined) {
this._renameCandidateProvidersCts.dispose(true);
this._renameCandidateProvidersCts = undefined;
}
}));
const inputResult = new DeferredPromise<RenameInputFieldResult | boolean>(); const inputResult = new DeferredPromise<RenameInputFieldResult | boolean>();
@ -433,6 +460,24 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
return inputResult.p; return inputResult.p;
} }
/**
* This allows selecting only part of the symbol name in the input field based on the selection in the editor
*/
private _getSelection(where: IRange, currentName: string): { start: number; end: number } {
assertType(this._editor.hasModel());
const selection = this._editor.getSelection();
let start = 0;
let end = currentName.length;
if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(where, selection)) {
start = Math.max(0, selection.startColumn - where.startColumn);
end = Math.min(where.endColumn, selection.endColumn) - where.startColumn;
}
return { start, end };
}
private _show(): void { private _show(): void {
this._trace('invoking _show'); this._trace('invoking _show');
this._editor.revealLineInCenterIfOutsideViewport(this._position!.lineNumber, ScrollType.Smooth); this._editor.revealLineInCenterIfOutsideViewport(this._position!.lineNumber, ScrollType.Smooth);
@ -508,8 +553,8 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
return this._editor.getTopForLineNumber(this._position!.lineNumber) - this._editor.getTopForLineNumber(firstLineInViewport); return this._editor.getTopForLineNumber(this._position!.lineNumber) - this._editor.getTopForLineNumber(firstLineInViewport);
} }
private _trace(...args: any[]) { private _trace(...args: unknown[]) {
this._logService.trace('RenameInputField', ...args); this._logService.trace('RenameWidget', ...args);
} }
} }

View file

@ -55,7 +55,7 @@ export interface IActionDescriptor {
*/ */
label: string; label: string;
/** /**
* Precondition rule. * Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts).
*/ */
precondition?: string; precondition?: string;
/** /**

2
src/vs/monaco.d.ts vendored
View file

@ -1251,7 +1251,7 @@ declare namespace monaco.editor {
*/ */
label: string; label: string;
/** /**
* Precondition rule. * Precondition rule. The value should be a [context key expression](https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts).
*/ */
precondition?: string; precondition?: string;
/** /**

View file

@ -59,7 +59,7 @@
.quick-input-header { .quick-input-header {
display: flex; display: flex;
padding: 8px 6px 6px 6px; padding: 8px 6px 2px 6px;
} }
.quick-input-widget.hidden-input .quick-input-header { .quick-input-widget.hidden-input .quick-input-header {
@ -323,12 +323,21 @@
background: none; background: none;
} }
/* Quick input separators as full-row item */
.quick-input-list .quick-input-list-separator-as-item { .quick-input-list .quick-input-list-separator-as-item {
font-weight: 600; padding: 4px 6px;
font-size: 12px; font-size: 12px;
} }
/* Quick input separators as full-row item */
.quick-input-list .quick-input-list-separator-as-item .label-name {
font-weight: 600;
}
.quick-input-list .quick-input-list-separator-as-item .label-description {
/* Override default description opacity so we don't have a contrast ratio issue. */
opacity: 1 !important;
}
/* Hide border when the item becomes the sticky one */ /* Hide border when the item becomes the sticky one */
.quick-input-list .monaco-tree-sticky-row .quick-input-list-entry.quick-input-list-separator-as-item.quick-input-list-separator-border { .quick-input-list .monaco-tree-sticky-row .quick-input-list-entry.quick-input-list-separator-as-item.quick-input-list-separator-border {
border-top-style: none; border-top-style: none;

View file

@ -92,6 +92,10 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
} }
} }
// Store the existing selection if there was one.
const visibleSelection = visibleQuickAccess?.picker?.valueSelection;
const visibleValue = visibleQuickAccess?.picker?.value;
// Create a picker for the provider to use with the initial value // Create a picker for the provider to use with the initial value
// and adjust the filtering to exclude the prefix from filtering // and adjust the filtering to exclude the prefix from filtering
const disposables = new DisposableStore(); const disposables = new DisposableStore();
@ -148,6 +152,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
// on the onDidHide event. // on the onDidHide event.
picker.show(); picker.show();
// If the previous picker had a selection and the value is unchanged, we should set that in the new picker.
if (visibleSelection && visibleValue === value) {
picker.valueSelection = visibleSelection;
}
// Pick mode: return with promise // Pick mode: return with promise
if (pick) { if (pick) {
return pickPromise?.p; return pickPromise?.p;

View file

@ -723,7 +723,15 @@ export class QuickPick<T extends IQuickPickItem> extends QuickInput implements I
return this.ui.keyMods; return this.ui.keyMods;
} }
set valueSelection(valueSelection: Readonly<[number, number]>) { get valueSelection() {
const selection = this.ui.inputBox.getSelection();
if (!selection) {
return undefined;
}
return [selection.start, selection.end];
}
set valueSelection(valueSelection: Readonly<[number, number]> | undefined) {
this._valueSelection = valueSelection; this._valueSelection = valueSelection;
this.valueSelectionUpdated = true; this.valueSelectionUpdated = true;
this.update(); this.update();
@ -1167,7 +1175,15 @@ export class InputBox extends QuickInput implements IInputBox {
this.update(); this.update();
} }
set valueSelection(valueSelection: Readonly<[number, number]>) { get valueSelection() {
const selection = this.ui.inputBox.getSelection();
if (!selection) {
return undefined;
}
return [selection.start, selection.end];
}
set valueSelection(valueSelection: Readonly<[number, number]> | undefined) {
this._valueSelection = valueSelection; this._valueSelection = valueSelection;
this.valueSelectionUpdated = true; this.valueSelectionUpdated = true;
this.update(); this.update();

View file

@ -59,6 +59,10 @@ export class QuickInputBox extends Disposable {
this.findInput.inputBox.select(range); this.findInput.inputBox.select(range);
} }
getSelection(): IRange | null {
return this.findInput.inputBox.getSelection();
}
isSelectionAtEnd(): boolean { isSelectionAtEnd(): boolean {
return this.findInput.inputBox.isSelectionAtEnd(); return this.findInput.inputBox.isSelectionAtEnd();
} }

View file

@ -277,9 +277,8 @@ class QuickPickSeparatorElement extends BaseQuickPickItemElement {
class QuickInputItemDelegate implements IListVirtualDelegate<IQuickPickElement> { class QuickInputItemDelegate implements IListVirtualDelegate<IQuickPickElement> {
getHeight(element: IQuickPickElement): number { getHeight(element: IQuickPickElement): number {
if (!element.item) { if (element instanceof QuickPickSeparatorElement) {
// must be a separator return 30;
return 24;
} }
return element.saneDetail ? 44 : 22; return element.saneDetail ? 44 : 22;
} }

View file

@ -20,17 +20,17 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
type AgentData = { interface AgentData {
dispose: () => void; dispose: () => void;
name: string; id: string;
extensionId: ExtensionIdentifier;
hasFollowups?: boolean; hasFollowups?: boolean;
}; }
@extHostNamedCustomer(MainContext.MainThreadChatAgents2) @extHostNamedCustomer(MainContext.MainThreadChatAgents2)
export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 { export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 {
@ -48,7 +48,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
@IInstantiationService private readonly _instantiationService: IInstantiationService, @IInstantiationService private readonly _instantiationService: IInstantiationService,
@IChatContributionService private readonly _chatContributionService: IChatContributionService,
) { ) {
super(); super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2);
@ -59,7 +58,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._register(this._chatService.onDidPerformUserAction(e => { this._register(this._chatService.onDidPerformUserAction(e => {
if (typeof e.agentId === 'string') { if (typeof e.agentId === 'string') {
for (const [handle, agent] of this._agents) { for (const [handle, agent] of this._agents) {
if (agent.name === e.agentId) { if (agent.id === e.agentId) {
if (e.action.kind === 'vote') { if (e.action.kind === 'vote') {
this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction); this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction);
} else { } else {
@ -76,10 +75,16 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._agents.deleteAndDispose(handle); this._agents.deleteAndDispose(handle);
} }
$registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void { $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void {
const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name); const staticAgentRegistration = this._chatAgentService.getAgent(id);
if (!staticAgentRegistration && !allowDynamic) { if (!staticAgentRegistration && !dynamicProps) {
throw new Error(`chatParticipant must be declared in package.json: ${name}`); if (this._chatAgentService.getAgentsByName(id).length) {
// Likely some extension authors will not adopt the new ID, so give a hint if they register a
// participant by name instead of ID.
throw new Error(`chatParticipant must be declared with an ID in package.json. The "id" property may be missing! "${id}"`);
}
throw new Error(`chatParticipant must be declared in package.json: ${id}`);
} }
const impl: IChatAgentImplementation = { const impl: IChatAgentImplementation = {
@ -107,10 +112,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
}; };
let disposable: IDisposable; let disposable: IDisposable;
if (!staticAgentRegistration && allowDynamic) { if (!staticAgentRegistration && dynamicProps) {
disposable = this._chatAgentService.registerDynamicAgent( disposable = this._chatAgentService.registerDynamicAgent(
{ {
id: name, id,
name: dynamicProps.name,
description: dynamicProps.description,
extensionId: extension, extensionId: extension,
metadata: revive(metadata), metadata: revive(metadata),
slashCommands: [], slashCommands: [],
@ -118,11 +125,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
}, },
impl); impl);
} else { } else {
disposable = this._chatAgentService.registerAgent(name, impl); disposable = this._chatAgentService.registerAgentImplementation(id, impl);
} }
this._agents.set(handle, { this._agents.set(handle, {
name, id: id,
extensionId: extension,
dispose: disposable.dispose, dispose: disposable.dispose,
hasFollowups: metadata.hasFollowups hasFollowups: metadata.hasFollowups
}); });
@ -134,7 +142,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
throw new Error(`No agent with handle ${handle} registered`); throw new Error(`No agent with handle ${handle} registered`);
} }
data.hasFollowups = metadataUpdate.hasFollowups; data.hasFollowups = metadataUpdate.hasFollowups;
this._chatAgentService.updateAgent(data.name, revive(metadataUpdate)); this._chatAgentService.updateAgent(data.id, revive(metadataUpdate));
} }
async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise<number | void> { async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise<number | void> {
@ -162,8 +170,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()).parts; const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()).parts;
const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart);
const thisAgentName = this._agents.get(handle)?.name; const thisAgentId = this._agents.get(handle)?.id;
if (agentPart?.agent.id !== thisAgentName) { if (agentPart?.agent.id !== thisAgentId) {
return; return;
} }

View file

@ -1427,10 +1427,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'mappedEditsProvider'); checkProposedApiEnabled(extension, 'mappedEditsProvider');
return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider);
}, },
createChatParticipant(name: string, handler: vscode.ChatExtendedRequestHandler) { createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) {
checkProposedApiEnabled(extension, 'chatParticipant'); checkProposedApiEnabled(extension, 'chatParticipant');
return extHostChatAgents2.createChatAgent(extension, name, handler); return extHostChatAgents2.createChatAgent(extension, id, handler);
}, },
createDynamicChatParticipant(id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
checkProposedApiEnabled(extension, 'chatParticipantAdditions');
return extHostChatAgents2.createDynamicChatAgent(extension, id, name, description, handler);
}
}; };
// namespace: lm // namespace: lm

View file

@ -1209,7 +1209,7 @@ export interface IExtensionChatAgentMetadata extends Dto<IChatAgentMetadata> {
} }
export interface MainThreadChatAgentsShape2 extends IDisposable { export interface MainThreadChatAgentsShape2 extends IDisposable {
$registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void; $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void;
$registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void;
$unregisterAgentCompletionsProvider(handle: number): void; $unregisterAgentCompletionsProvider(handle: number): void;
$updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void;

View file

@ -122,7 +122,7 @@ class ChatAgentResponseStream {
}, },
push(part) { push(part) {
throwIfDone(this.push); throwIfDone(this.push);
const dto = typeConvert.ChatResponsePart.to(part); const dto = typeConvert.ChatResponsePart.to(part, that._commandsConverter, that._sessionDisposables);
_report(dto); _report(dto);
return this; return this;
}, },
@ -166,12 +166,21 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2);
} }
createChatAgent(extension: IExtensionDescription, name: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
const handle = ExtHostChatAgents2._idPool++; const handle = ExtHostChatAgents2._idPool++;
const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler);
this._agents.set(handle, agent); this._agents.set(handle, agent);
this._proxy.$registerAgent(handle, extension.identifier, name, {}, isProposedApiEnabled(extension, 'chatParticipantAdditions')); this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined);
return agent.apiAgent;
}
createDynamicChatAgent(extension: IExtensionDescription, id: string, name: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant {
const handle = ExtHostChatAgents2._idPool++;
const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler);
this._agents.set(handle, agent);
this._proxy.$registerAgent(handle, extension.identifier, id, {}, { name, description });
return agent.apiAgent; return agent.apiAgent;
} }
@ -231,11 +240,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
{ ...ehResult, metadata: undefined }; { ...ehResult, metadata: undefined };
// REQUEST turn // REQUEST turn
res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), { extensionId: '', name: h.request.agentId })); res.push(new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.ChatAgentResolvedVariable.to), h.request.agentId));
// RESPONSE turn // RESPONSE turn
const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.fromContent(r, this.commands.converter))); const parts = coalesce(h.response.map(r => typeConvert.ChatResponsePart.fromContent(r, this.commands.converter)));
res.push(new extHostTypes.ChatResponseTurn(parts, result, { extensionId: '', name: h.request.agentId }, h.request.command)); res.push(new extHostTypes.ChatResponseTurn(parts, result, h.request.agentId, h.request.command));
} }
return res; return res;
@ -338,7 +347,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
class ExtHostChatAgent { class ExtHostChatAgent {
private _followupProvider: vscode.ChatFollowupProvider | undefined; private _followupProvider: vscode.ChatFollowupProvider | undefined;
private _description: string | undefined;
private _fullName: string | undefined; private _fullName: string | undefined;
private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined; private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined;
private _isDefault: boolean | undefined; private _isDefault: boolean | undefined;
@ -437,7 +445,6 @@ class ExtHostChatAgent {
updateScheduled = true; updateScheduled = true;
queueMicrotask(() => { queueMicrotask(() => {
this._proxy.$updateAgent(this._handle, { this._proxy.$updateAgent(this._handle, {
description: this._description,
fullName: this._fullName, fullName: this._fullName,
icon: !this._iconPath ? undefined : icon: !this._iconPath ? undefined :
this._iconPath instanceof URI ? this._iconPath : this._iconPath instanceof URI ? this._iconPath :
@ -463,16 +470,9 @@ class ExtHostChatAgent {
const that = this; const that = this;
return { return {
get name() { get id() {
return that.id; return that.id;
}, },
get description() {
return that._description ?? '';
},
set description(v) {
that._description = v;
updateMetadataSoon();
},
get fullName() { get fullName() {
checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); checkProposedApiEnabled(that.extension, 'defaultChatParticipant');
return that._fullName ?? that.extension.displayName ?? that.extension.name; return that._fullName ?? that.extension.displayName ?? that.extension.name;

View file

@ -109,8 +109,13 @@ class ChatVariableResolverResponseStream {
}, },
push(part) { push(part) {
throwIfDone(this.push); throwIfDone(this.push);
const dto = typeConvert.ChatResponsePart.to(part);
_report(dto as IChatVariableResolverProgressDto); if (part instanceof extHostTypes.ChatResponseReferencePart) {
_report(typeConvert.ChatResponseReferencePart.to(part));
} else if (part instanceof extHostTypes.ChatResponseProgressPart) {
_report(typeConvert.ChatResponseProgressPart.to(part));
}
return this; return this;
} }
}; };

View file

@ -2454,7 +2454,7 @@ export namespace ChatResponseReferencePart {
export namespace ChatResponsePart { export namespace ChatResponsePart {
export function to(part: vscode.ChatResponsePart): extHostProtocol.IChatProgressDto { export function to(part: vscode.ChatResponsePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto {
if (part instanceof types.ChatResponseMarkdownPart) { if (part instanceof types.ChatResponseMarkdownPart) {
return ChatResponseMarkdownPart.to(part); return ChatResponseMarkdownPart.to(part);
} else if (part instanceof types.ChatResponseAnchorPart) { } else if (part instanceof types.ChatResponseAnchorPart) {
@ -2465,6 +2465,8 @@ export namespace ChatResponsePart {
return ChatResponseProgressPart.to(part); return ChatResponseProgressPart.to(part);
} else if (part instanceof types.ChatResponseFileTreePart) { } else if (part instanceof types.ChatResponseFileTreePart) {
return ChatResponseFilesPart.to(part); return ChatResponseFilesPart.to(part);
} else if (part instanceof types.ChatResponseCommandButtonPart) {
return ChatResponseCommandButtonPart.to(part, commandsConverter, commandDisposables);
} }
return { return {
kind: 'content', kind: 'content',
@ -2546,7 +2548,7 @@ export namespace ChatResponseProgress {
}; };
} else if ('participant' in progress) { } else if ('participant' in progress) {
checkProposedApiEnabled(extension, 'chatParticipantAdditions'); checkProposedApiEnabled(extension, 'chatParticipantAdditions');
return { agentName: progress.participant, command: progress.command, kind: 'agentDetection' }; return { agentId: progress.participant, command: progress.command, kind: 'agentDetection' };
} else if ('message' in progress) { } else if ('message' in progress) {
return { content: MarkdownString.from(progress.message), kind: 'progressMessage' }; return { content: MarkdownString.from(progress.message), kind: 'progressMessage' };
} else { } else {

View file

@ -4310,7 +4310,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn {
readonly prompt: string, readonly prompt: string,
readonly command: string | undefined, readonly command: string | undefined,
readonly variables: vscode.ChatResolvedVariable[], readonly variables: vscode.ChatResolvedVariable[],
readonly participant: { extensionId: string; name: string }, readonly participant: string,
) { } ) { }
} }
@ -4319,7 +4319,7 @@ export class ChatResponseTurn implements vscode.ChatResponseTurn {
constructor( constructor(
readonly response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart>, readonly response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart>,
readonly result: vscode.ChatResult, readonly result: vscode.ChatResult,
readonly participant: { extensionId: string; name: string }, readonly participant: string,
readonly command?: string readonly command?: string
) { } ) { }
} }

View file

@ -148,7 +148,7 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut
} }
private async enableEntitlements(session: AuthenticationSession) { private async enableEntitlements(session: AuthenticationSession) {
const isInternal = isInternalTelemetry(this.productService, this.configurationService) ?? true; const isInternal = isInternalTelemetry(this.productService, this.configurationService);
const showAccountsBadge = this.configurationService.inspect<boolean>(accountsBadgeConfigKey).value ?? false; const showAccountsBadge = this.configurationService.inspect<boolean>(accountsBadgeConfigKey).value ?? false;
const showWelcomeView = this.configurationService.inspect<boolean>(chatWelcomeViewConfigKey).value ?? false; const showWelcomeView = this.configurationService.inspect<boolean>(chatWelcomeViewConfigKey).value ?? false;

View file

@ -128,7 +128,7 @@ export class ChatSubmitSecondaryAgentEditorAction extends EditorAction2 {
if (widget.getInput().match(/^\s*@/)) { if (widget.getInput().match(/^\s*@/)) {
widget.acceptInput(); widget.acceptInput();
} else { } else {
widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.id}`); widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.name}`);
} }
} }
} }

View file

@ -251,7 +251,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
executeImmediately: true executeImmediately: true
}, async (prompt, progress) => { }, async (prompt, progress) => {
const defaultAgent = chatAgentService.getDefaultAgent(); const defaultAgent = chatAgentService.getDefaultAgent();
const agents = chatAgentService.getRegisteredAgents(); const agents = chatAgentService.getAgents();
// Report prefix // Report prefix
if (defaultAgent?.metadata.helpTextPrefix) { if (defaultAgent?.metadata.helpTextPrefix) {
@ -270,7 +270,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
const agentWithLeader = `${chatAgentLeader}${a.id}`; const agentWithLeader = `${chatAgentLeader}${a.id}`;
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` }; const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`; const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.description}`;
const commandText = a.slashCommands.map(c => { const commandText = a.slashCommands.map(c => {
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));

View file

@ -112,6 +112,7 @@ export interface IChatWidget {
readonly providerId: string; readonly providerId: string;
readonly supportsFileReferences: boolean; readonly supportsFileReferences: boolean;
readonly parsedInput: IParsedChatRequest; readonly parsedInput: IParsedChatRequest;
lastSelectedAgent: IChatAgentData | undefined;
getContrib<T extends IChatWidgetContrib>(id: string): T | undefined; getContrib<T extends IChatWidgetContrib>(id: string): T | undefined;
reveal(item: ChatTreeItem): void; reveal(item: ChatTreeItem): void;

View file

@ -3,11 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { Codicon } from 'vs/base/common/codicons'; import { Codicon } from 'vs/base/common/codicons';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { localize, localize2 } from 'vs/nls'; import { localize, localize2 } from 'vs/nls';
import { registerAction2 } from 'vs/platform/actions/common/actions'; import { registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService'; import { IProductService } from 'vs/platform/product/common/productService';
@ -20,7 +22,8 @@ import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chat
import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions';
import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane';
import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatContributionService, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry';
@ -64,20 +67,24 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi
const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({
extensionPoint: 'chatParticipants', extensionPoint: 'chatParticipants',
jsonSchema: { jsonSchema: {
description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'), description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a chat participant'),
type: 'array', type: 'array',
items: { items: {
additionalProperties: false, additionalProperties: false,
type: 'object', type: 'object',
defaultSnippets: [{ body: { name: '', description: '' } }], defaultSnippets: [{ body: { name: '', description: '' } }],
required: ['name'], required: ['name', 'id'],
properties: { properties: {
id: {
description: localize('chatParticipantId', "A unique id for this chat participant."),
type: 'string'
},
name: { name: {
description: localize('chatParticipantName', "Unique name for this Chat Participant."), description: localize('chatParticipantName', "User-facing display name for this chat participant. The user will use '@' with this name to invoke the participant."),
type: 'string' type: 'string'
}, },
description: { description: {
description: localize('chatParticipantDescription', "A description of this Chat Participant, shown in the UI."), description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."),
type: 'string' type: 'string'
}, },
isDefault: { isDefault: {
@ -92,7 +99,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
} }
}, },
commands: { commands: {
markdownDescription: localize('chatCommandsDescription', "Commands available for this Chat Participant, which the user can invoke with a `/`."), markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."),
type: 'array', type: 'array',
items: { items: {
additionalProperties: false, additionalProperties: false,
@ -131,7 +138,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
} }
}, },
locations: { locations: {
markdownDescription: localize('chatLocationsDescription', "Locations in which this Chat Participant is available."), markdownDescription: localize('chatLocationsDescription', "Locations in which this chat participant is available."),
type: 'array', type: 'array',
default: ['panel'], default: ['panel'],
items: { items: {
@ -158,12 +165,14 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
private _welcomeViewDescriptor?: IViewDescriptor; private _welcomeViewDescriptor?: IViewDescriptor;
private _viewContainer: ViewContainer; private _viewContainer: ViewContainer;
private _registrationDisposables = new Map<string, IDisposable>(); private _registrationDisposables = new Map<string, IDisposable>();
private _participantRegistrationDisposables = new DisposableMap<string>();
constructor( constructor(
@IChatContributionService readonly _chatContributionService: IChatContributionService, @IChatContributionService private readonly _chatContributionService: IChatContributionService,
@IProductService readonly productService: IProductService, @IChatAgentService private readonly _chatAgentService: IChatAgentService,
@IContextKeyService readonly contextService: IContextKeyService, @IProductService private readonly productService: IProductService,
@ILogService readonly logService: ILogService, @IContextKeyService private readonly contextService: IContextKeyService,
@ILogService private readonly logService: ILogService,
) { ) {
this._viewContainer = this.registerViewContainer(); this._viewContainer = this.registerViewContainer();
this.registerListeners(); this.registerListeners();
@ -243,13 +252,34 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
continue; continue;
} }
this._chatContributionService.registerChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); if (!providerDescriptor.id || !providerDescriptor.name) {
this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`);
continue;
}
this._participantRegistrationDisposables.set(
getParticipantKey(extension.description.identifier, providerDescriptor.name),
this._chatAgentService.registerAgent(
providerDescriptor.id,
{
extensionId: extension.description.identifier,
id: providerDescriptor.id,
description: providerDescriptor.description,
metadata: {},
name: providerDescriptor.name,
isDefault: providerDescriptor.isDefault,
defaultImplicitVariables: providerDescriptor.defaultImplicitVariables,
locations: isNonEmptyArray(providerDescriptor.locations) ?
providerDescriptor.locations.map(ChatAgentLocation.fromRaw) :
[ChatAgentLocation.Panel],
slashCommands: providerDescriptor.commands ?? []
} satisfies IChatAgentData));
} }
} }
for (const extension of delta.removed) { for (const extension of delta.removed) {
for (const providerDescriptor of extension.value) { for (const providerDescriptor of extension.value) {
this._chatContributionService.deregisterChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.name));
} }
} }
}); });
@ -314,15 +344,14 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup);
function getParticipantKey(participant: IChatParticipantContribution): string { function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string {
return `${participant.extensionId.value}_${participant.name}`; return `${extensionId.value}_${participantName}`;
} }
export class ChatContributionService implements IChatContributionService { export class ChatContributionService implements IChatContributionService {
declare _serviceBrand: undefined; declare _serviceBrand: undefined;
private _registeredProviders = new Map<string, IChatProviderContribution>(); private _registeredProviders = new Map<string, IChatProviderContribution>();
private _registeredParticipants = new Map<string, IChatParticipantContribution>();
constructor( constructor(
) { } ) { }
@ -339,19 +368,7 @@ export class ChatContributionService implements IChatContributionService {
this._registeredProviders.delete(providerId); this._registeredProviders.delete(providerId);
} }
public registerChatParticipant(participant: IChatParticipantContribution): void {
this._registeredParticipants.set(getParticipantKey(participant), participant);
}
public deregisterChatParticipant(participant: IChatParticipantContribution): void {
this._registeredParticipants.delete(getParticipantKey(participant));
}
public get registeredProviders(): IChatProviderContribution[] { public get registeredProviders(): IChatProviderContribution[] {
return Array.from(this._registeredProviders.values()); return Array.from(this._registeredProviders.values());
} }
public get registeredParticipants(): IChatParticipantContribution[] {
return Array.from(this._registeredParticipants.values());
}
} }

View file

@ -359,7 +359,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void {
let progressMsg: string = ''; let progressMsg: string = '';
if (element.agent && !element.agent.isDefault) { if (element.agent && !element.agent.isDefault) {
let usingMsg = chatAgentLeader + element.agent.id; let usingMsg = chatAgentLeader + element.agent.name;
if (element.slashCommand) { if (element.slashCommand) {
usingMsg += ` ${chatSubcommandLeader}${element.slashCommand.name}`; usingMsg += ` ${chatSubcommandLeader}${element.slashCommand.name}`;
} }

View file

@ -11,7 +11,7 @@ import { Location } from 'vs/editor/common/languages';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILabelService } from 'vs/platform/label/common/label'; import { ILabelService } from 'vs/platform/label/common/label';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { contentRefUrl } from '../common/annotations'; import { contentRefUrl } from '../common/annotations';
const variableRefUrl = 'http://_vscodedecoration_'; const variableRefUrl = 'http://_vscodedecoration_';
@ -31,7 +31,9 @@ export class ChatMarkdownDecorationsRenderer {
} else { } else {
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI) const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
|| undefined; || undefined;
const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : ''; const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) :
part instanceof ChatRequestAgentPart ? part.agent.id :
'';
result += `[${part.text}](${variableRefUrl}?${title})`; result += `[${part.text}](${variableRefUrl}?${title})`;
} }
@ -106,4 +108,3 @@ export class ChatMarkdownDecorationsRenderer {
} }
} }
} }

View file

@ -145,7 +145,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
private parsedChatRequest: IParsedChatRequest | undefined; private parsedChatRequest: IParsedChatRequest | undefined;
get parsedInput() { get parsedInput() {
if (this.parsedChatRequest === undefined) { if (this.parsedChatRequest === undefined) {
this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput()); this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), { selectedAgent: this._lastSelectedAgent });
} }
return this.parsedChatRequest; return this.parsedChatRequest;
@ -212,6 +212,15 @@ export class ChatWidget extends Disposable implements IChatWidget {
})); }));
} }
private _lastSelectedAgent: IChatAgentData | undefined;
set lastSelectedAgent(agent: IChatAgentData | undefined) {
this._lastSelectedAgent = agent;
}
get lastSelectedAgent(): IChatAgentData | undefined {
return this._lastSelectedAgent;
}
get supportsFileReferences(): boolean { get supportsFileReferences(): boolean {
return !!this.viewOptions.supportsFileReferences; return !!this.viewOptions.supportsFileReferences;
} }
@ -655,7 +664,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
'query' in opts ? opts.query : 'query' in opts ? opts.query :
`${opts.prefix} ${editorValue}`; `${opts.prefix} ${editorValue}`;
const isUserQuery = !opts || 'prefix' in opts; const isUserQuery = !opts || 'prefix' in opts;
const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled); const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, this.inputPart.implicitContextEnabled, { selectedAgent: this._lastSelectedAgent });
if (result) { if (result) {
const inputState = this.collectInputState(); const inputState = this.collectInputState();

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { Position } from 'vs/editor/common/core/position'; import { Position } from 'vs/editor/common/core/position';
@ -14,7 +15,8 @@ import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList }
import { ITextModel } from 'vs/editor/common/model'; import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
@ -37,8 +39,8 @@ const placeholderDecorationType = 'chat-session-detail';
const slashCommandTextDecorationType = 'chat-session-text'; const slashCommandTextDecorationType = 'chat-session-text';
const variableTextDecorationType = 'chat-variable-text'; const variableTextDecorationType = 'chat-variable-text';
function agentAndCommandToKey(agent: string, subcommand: string | undefined): string { function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string {
return subcommand ? `${agent}__${subcommand}` : agent; return subcommand ? `${agent.id}__${subcommand}` : agent.id;
} }
class InputEditorDecorations extends Disposable { class InputEditorDecorations extends Disposable {
@ -70,7 +72,7 @@ class InputEditorDecorations extends Disposable {
this.updateInputEditorDecorations(); this.updateInputEditorDecorations();
})); }));
this._register(this.widget.onDidSubmitAgent((e) => { this._register(this.widget.onDidSubmitAgent((e) => {
this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent.id, e.slashCommand?.name)); this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent, e.slashCommand?.name));
})); }));
this._register(this.chatAgentService.onDidChangeAgents(() => this.updateInputEditorDecorations())); this._register(this.chatAgentService.onDidChangeAgents(() => this.updateInputEditorDecorations()));
@ -135,7 +137,7 @@ class InputEditorDecorations extends Disposable {
}, },
renderOptions: { renderOptions: {
after: { after: {
contentText: viewModel.inputPlaceholder ?? defaultAgent?.metadata.description ?? '', contentText: viewModel.inputPlaceholder ?? defaultAgent?.description ?? '',
color: this.getPlaceholderColor() color: this.getPlaceholderColor()
} }
} }
@ -172,14 +174,14 @@ class InputEditorDecorations extends Disposable {
const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart); const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart);
if (onlyAgentAndWhitespace) { if (onlyAgentAndWhitespace) {
// Agent reference with no other text - show the placeholder // Agent reference with no other text - show the placeholder
const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, undefined)); const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, undefined));
const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentPart.agent.metadata.followupPlaceholder; const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentPart.agent.metadata.followupPlaceholder;
if (agentPart.agent.metadata.description && exactlyOneSpaceAfterPart(agentPart)) { if (agentPart.agent.description && exactlyOneSpaceAfterPart(agentPart)) {
placeholderDecoration = [{ placeholderDecoration = [{
range: getRangeForPlaceholder(agentPart), range: getRangeForPlaceholder(agentPart),
renderOptions: { renderOptions: {
after: { after: {
contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.metadata.description, contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.description,
color: this.getPlaceholderColor(), color: this.getPlaceholderColor(),
} }
} }
@ -190,7 +192,7 @@ class InputEditorDecorations extends Disposable {
const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart);
if (onlyAgentCommandAndWhitespace) { if (onlyAgentCommandAndWhitespace) {
// Agent reference and subcommand with no other text - show the placeholder // Agent reference and subcommand with no other text - show the placeholder
const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, agentSubcommandPart.command.name)); const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name));
const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder; const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder;
if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) { if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) {
placeholderDecoration = [{ placeholderDecoration = [{
@ -209,9 +211,10 @@ class InputEditorDecorations extends Disposable {
const textDecorations: IDecorationOptions[] | undefined = []; const textDecorations: IDecorationOptions[] | undefined = [];
if (agentPart) { if (agentPart) {
textDecorations.push({ range: agentPart.editorRange }); const agentHover = `(${agentPart.agent.id}) ${agentPart.agent.description}`;
textDecorations.push({ range: agentPart.editorRange, hoverMessage: new MarkdownString(agentHover) });
if (agentSubcommandPart) { if (agentSubcommandPart) {
textDecorations.push({ range: agentSubcommandPart.editorRange }); textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) });
} }
} }
@ -246,9 +249,9 @@ class InputEditorSlashCommandMode extends Disposable {
private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) { private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) {
let value: string | undefined; let value: string | undefined;
if (slashCommand && slashCommand.isSticky) { if (slashCommand && slashCommand.isSticky) {
value = `${chatAgentLeader}${agent.id} ${chatSubcommandLeader}${slashCommand.name} `; value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `;
} else if (agent.metadata.isSticky) { } else if (agent.metadata.isSticky) {
value = `${chatAgentLeader}${agent.id} `; value = `${chatAgentLeader}${agent.name} `;
} }
if (value) { if (value) {
@ -347,13 +350,18 @@ class AgentCompletions extends Disposable {
const agents = this.chatAgentService.getAgents() const agents = this.chatAgentService.getAgents()
.filter(a => !a.isDefault); .filter(a => !a.isDefault);
return <CompletionList>{ return <CompletionList>{
suggestions: agents.map((c, i) => { suggestions: agents.map((a, i) => {
const withAt = `@${c.id}`; const withAt = `@${a.name}`;
const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id);
return <CompletionItem>{ return <CompletionItem>{
label: withAt, // Leading space is important because detail has no space at the start by design
label: isDupe ?
{ label: withAt, description: a.description, detail: ` (${a.id})` } :
withAt,
insertText: `${withAt} `, insertText: `${withAt} `,
detail: c.metadata.description, detail: a.description,
range: new Range(1, 1, 1, 1), range: new Range(1, 1, 1, 1),
command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: a, widget } satisfies AssignSelectedAgentActionArgs] },
kind: CompletionItemKind.Text, // The icons are disabled here anyway kind: CompletionItemKind.Text, // The icons are disabled here anyway
}; };
}) })
@ -431,31 +439,37 @@ class AgentCompletions extends Disposable {
const justAgents: CompletionItem[] = agents const justAgents: CompletionItem[] = agents
.filter(a => !a.isDefault) .filter(a => !a.isDefault)
.map(agent => { .map(agent => {
const agentLabel = `${chatAgentLeader}${agent.id}`; const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id);
const detail = agent.description;
const agentLabel = `${chatAgentLeader}${agent.name} (${agent.id})`;
return { return {
label: { label: agentLabel, description: agent.metadata.description }, label: isDupe ?
filterText: `${chatSubcommandLeader}${agent.id}`, { label: agentLabel, description: agent.description, detail: ` (${agent.id})` } :
agentLabel,
detail,
filterText: `${chatSubcommandLeader}${agent.name}`,
insertText: `${agentLabel} `, insertText: `${agentLabel} `,
range: new Range(1, 1, 1, 1), range: new Range(1, 1, 1, 1),
kind: CompletionItemKind.Text, kind: CompletionItemKind.Text,
sortText: `${chatSubcommandLeader}${agent.id}`, sortText: `${chatSubcommandLeader}${agent.name}`,
}; };
}); });
return { return {
suggestions: justAgents.concat( suggestions: justAgents.concat(
agents.flatMap(agent => agent.slashCommands.map((c, i) => { agents.flatMap(agent => agent.slashCommands.map((c, i) => {
const agentLabel = `${chatAgentLeader}${agent.id}`; const agentLabel = `${chatAgentLeader}${agent.name}`;
const withSlash = `${chatSubcommandLeader}${c.name}`; const withSlash = `${chatSubcommandLeader}${c.name}`;
return { return {
label: { label: withSlash, description: agentLabel }, label: { label: withSlash, description: agentLabel },
filterText: `${chatSubcommandLeader}${agent.id}${c.name}`, filterText: `${chatSubcommandLeader}${agent.name}${c.name}`,
commitCharacters: [' '], commitCharacters: [' '],
insertText: `${agentLabel} ${withSlash} `, insertText: `${agentLabel} ${withSlash} `,
detail: `(${agentLabel}) ${c.description ?? ''}`, detail: `(${agentLabel}) ${c.description ?? ''}`,
range: new Range(1, 1, 1, 1), range: new Range(1, 1, 1, 1),
kind: CompletionItemKind.Text, // The icons are disabled here anyway kind: CompletionItemKind.Text, // The icons are disabled here anyway
sortText: `${chatSubcommandLeader}${agent.id}${c.name}`, sortText: `${chatSubcommandLeader}${agent.name}${c.name}`,
} satisfies CompletionItem; } satisfies CompletionItem;
}))) })))
}; };
@ -465,6 +479,32 @@ class AgentCompletions extends Disposable {
} }
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually);
interface AssignSelectedAgentActionArgs {
agent: IChatAgentData;
widget: IChatWidget;
}
class AssignSelectedAgentAction extends Action2 {
static readonly ID = 'workbench.action.chat.assignSelectedAgent';
constructor() {
super({
id: AssignSelectedAgentAction.ID,
title: '' // not displayed
});
}
async run(accessor: ServicesAccessor, ...args: any[]) {
const arg: AssignSelectedAgentActionArgs = args[0];
if (!arg || !arg.widget || !arg.agent) {
return;
}
arg.widget.lastSelectedAgent = arg.agent;
}
}
registerAction2(AssignSelectedAgentAction);
class BuiltinDynamicCompletions extends Disposable { class BuiltinDynamicCompletions extends Disposable {
private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag
@ -592,12 +632,14 @@ class ChatTokenDeleter extends Disposable {
const parser = this.instantiationService.createInstance(ChatRequestParser); const parser = this.instantiationService.createInstance(ChatRequestParser);
const inputValue = this.widget.inputEditor.getValue(); const inputValue = this.widget.inputEditor.getValue();
let previousInputValue: string | undefined; let previousInputValue: string | undefined;
let previousSelectedAgent: IChatAgentData | undefined;
// A simple heuristic to delete the previous token when the user presses backspace. // A simple heuristic to delete the previous token when the user presses backspace.
// The sophisticated way to do this would be to have a parse tree that can be updated incrementally. // The sophisticated way to do this would be to have a parse tree that can be updated incrementally.
this._register(this.widget.inputEditor.onDidChangeModelContent(e => { this._register(this.widget.inputEditor.onDidChangeModelContent(e => {
if (!previousInputValue) { if (!previousInputValue) {
previousInputValue = inputValue; previousInputValue = inputValue;
previousSelectedAgent = this.widget.lastSelectedAgent;
} }
// Don't try to handle multicursor edits right now // Don't try to handle multicursor edits right now
@ -605,7 +647,7 @@ class ChatTokenDeleter extends Disposable {
// If this was a simple delete, try to find out whether it was inside a token // If this was a simple delete, try to find out whether it was inside a token
if (!change.text && this.widget.viewModel) { if (!change.text && this.widget.viewModel) {
const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue); const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, { selectedAgent: previousSelectedAgent });
// For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping
const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart); const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart);
@ -625,6 +667,7 @@ class ChatTokenDeleter extends Disposable {
} }
previousInputValue = this.widget.inputEditor.getValue(); previousInputValue = this.widget.inputEditor.getValue();
previousSelectedAgent = this.widget.lastSelectedAgent;
})); }));
} }
} }

View file

@ -3,19 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray, distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator'; import { Iterable } from 'vs/base/common/iterator';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables'; import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { ProviderResult } from 'vs/editor/common/languages'; import { ProviderResult } from 'vs/editor/common/languages';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatContributionService, IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
@ -47,6 +46,8 @@ export namespace ChatAgentLocation {
export interface IChatAgentData { export interface IChatAgentData {
id: string; id: string;
name: string;
description?: string;
extensionId: ExtensionIdentifier; extensionId: ExtensionIdentifier;
/** The agent invoked when no agent is specified */ /** The agent invoked when no agent is specified */
isDefault?: boolean; isDefault?: boolean;
@ -79,7 +80,6 @@ export interface IChatRequesterInformation {
} }
export interface IChatAgentMetadata { export interface IChatAgentMetadata {
description?: string;
helpTextPrefix?: string | IMarkdownString; helpTextPrefix?: string | IMarkdownString;
helpTextVariablesPrefix?: string | IMarkdownString; helpTextVariablesPrefix?: string | IMarkdownString;
helpTextPostfix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString;
@ -118,86 +118,102 @@ export interface IChatAgentResult {
export const IChatAgentService = createDecorator<IChatAgentService>('chatAgentService'); export const IChatAgentService = createDecorator<IChatAgentService>('chatAgentService');
interface IChatAgentEntry {
data: IChatAgentData;
impl?: IChatAgentImplementation;
}
export interface IChatAgentService { export interface IChatAgentService {
_serviceBrand: undefined; _serviceBrand: undefined;
/** /**
* undefined when an agent was removed IChatAgent * undefined when an agent was removed IChatAgent
*/ */
readonly onDidChangeAgents: Event<IChatAgent | undefined>; readonly onDidChangeAgents: Event<IChatAgent | undefined>;
registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; registerAgent(id: string, data: IChatAgentData): IDisposable;
registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable;
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable;
invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>; invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]>; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]>;
getAgents(): IChatAgentData[];
getRegisteredAgents(): Array<IChatAgentData>;
getActivatedAgents(): Array<IChatAgent>;
getAgent(id: string): IChatAgentData | undefined; getAgent(id: string): IChatAgentData | undefined;
getAgents(): IChatAgentData[];
getActivatedAgents(): Array<IChatAgent>;
getAgentsByName(name: string): IChatAgentData[];
getDefaultAgent(): IChatAgent | undefined; getDefaultAgent(): IChatAgent | undefined;
getSecondaryAgent(): IChatAgentData | undefined; getSecondaryAgent(): IChatAgentData | undefined;
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void;
} }
export class ChatAgentService extends Disposable implements IChatAgentService { export class ChatAgentService implements IChatAgentService {
public static readonly AGENT_LEADER = '@'; public static readonly AGENT_LEADER = '@';
declare _serviceBrand: undefined; declare _serviceBrand: undefined;
private readonly _agents = new Map<string, { data: IChatAgentData; impl?: IChatAgentImplementation }>(); private _agents: IChatAgentEntry[] = [];
private readonly _onDidChangeAgents = this._register(new Emitter<IChatAgent | undefined>()); private readonly _onDidChangeAgents = new Emitter<IChatAgent | undefined>();
readonly onDidChangeAgents: Event<IChatAgent | undefined> = this._onDidChangeAgents.event; readonly onDidChangeAgents: Event<IChatAgent | undefined> = this._onDidChangeAgents.event;
constructor( constructor(
@IChatContributionService private chatContributionService: IChatContributionService, @IContextKeyService private readonly contextKeyService: IContextKeyService
@IContextKeyService private contextKeyService: IContextKeyService, ) { }
) {
super();
}
override dispose(): void { registerAgent(id: string, data: IChatAgentData): IDisposable {
super.dispose(); const existingAgent = this.getAgent(id);
this._agents.clear(); if (existingAgent) {
} throw new Error(`Agent already registered: ${JSON.stringify(id)}`);
registerAgent(name: string, agentImpl: IChatAgentImplementation): IDisposable {
if (this._agents.has(name)) {
// TODO not keyed by name, dupes allowed between extensions
throw new Error(`Already registered an agent with id ${name}`);
} }
const data = this.getAgent(name); const that = this;
if (!data) { const commands = data.slashCommands;
throw new Error(`Unknown agent: ${name}`); data = {
...data,
get slashCommands() {
return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when)));
}
};
const entry = { data };
this._agents.push(entry);
return toDisposable(() => {
this._agents = this._agents.filter(a => a !== entry);
this._onDidChangeAgents.fire(undefined);
});
}
registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable {
const entry = this._getAgentEntry(id);
if (!entry) {
throw new Error(`Unknown agent: ${JSON.stringify(id)}`);
} }
const agent = { data: data, impl: agentImpl }; if (entry.impl) {
this._agents.set(name, agent); throw new Error(`Agent already has implementation: ${JSON.stringify(id)}`);
this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); }
entry.impl = agentImpl;
this._onDidChangeAgents.fire(new MergedChatAgent(entry.data, agentImpl));
return toDisposable(() => { return toDisposable(() => {
if (this._agents.delete(name)) { entry.impl = undefined;
this._onDidChangeAgents.fire(undefined); this._onDidChangeAgents.fire(undefined);
}
}); });
} }
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable {
const agent = { data, impl: agentImpl }; const agent = { data, impl: agentImpl };
this._agents.set(data.id, agent); this._agents.push(agent);
this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl));
return toDisposable(() => { return toDisposable(() => {
if (this._agents.delete(data.id)) { this._agents = this._agents.filter(a => a !== agent);
this._onDidChangeAgents.fire(undefined); this._onDidChangeAgents.fire(undefined);
}
}); });
} }
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { updateAgent(id: string, updateMetadata: IChatAgentMetadata): void {
const agent = this._agents.get(id); const agent = this._getAgentEntry(id);
if (!agent?.impl) { if (!agent?.impl) {
throw new Error(`No activated agent with id ${id} registered`); throw new Error(`No activated agent with id ${JSON.stringify(id)} registered`);
} }
agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; agent.data.metadata = { ...agent.data.metadata, ...updateMetadata };
this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl));
@ -212,35 +228,19 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data;
} }
getRegisteredAgents(): Array<IChatAgentData> { private _getAgentEntry(id: string): IChatAgentEntry | undefined {
const that = this; return this._agents.find(a => a.data.id === id);
return this.chatContributionService.registeredParticipants.map(p => ( }
{
extensionId: p.extensionId, getAgent(id: string): IChatAgentData | undefined {
id: p.name, return this._getAgentEntry(id)?.data;
metadata: this._agents.has(p.name) ? this._agents.get(p.name)!.data.metadata : { description: p.description },
isDefault: p.isDefault,
defaultImplicitVariables: p.defaultImplicitVariables,
locations: isNonEmptyArray(p.locations) ? p.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel],
get slashCommands() {
const commands = p.commands ?? [];
return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when)));
}
} satisfies IChatAgentData));
} }
/** /**
* Returns all agent datas that exist- static registered and dynamic ones. * Returns all agent datas that exist- static registered and dynamic ones.
*/ */
getAgents(): IChatAgentData[] { getAgents(): IChatAgentData[] {
const registeredAgents = this.getRegisteredAgents(); return this._agents.map(entry => entry.data);
const dynamicAgents = Array.from(this._agents.values()).map(a => a.data);
const all = [
...registeredAgents,
...dynamicAgents
];
return distinct(all, a => a.id);
} }
getActivatedAgents(): IChatAgent[] { getActivatedAgents(): IChatAgent[] {
@ -249,12 +249,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
.map(a => new MergedChatAgent(a.data, a.impl!)); .map(a => new MergedChatAgent(a.data, a.impl!));
} }
getAgent(id: string): IChatAgentData | undefined { getAgentsByName(name: string): IChatAgentData[] {
return this.getAgents().find(a => a.id === id); return this.getAgents().filter(a => a.name === name);
} }
async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> {
const data = this._agents.get(id); const data = this._getAgentEntry(id);
if (!data?.impl) { if (!data?.impl) {
throw new Error(`No activated agent with id ${id}`); throw new Error(`No activated agent with id ${id}`);
} }
@ -263,7 +263,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
} }
async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> { async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> {
const data = this._agents.get(id); const data = this._getAgentEntry(id);
if (!data?.impl) { if (!data?.impl) {
throw new Error(`No activated agent with id ${id}`); throw new Error(`No activated agent with id ${id}`);
} }
@ -283,6 +283,8 @@ export class MergedChatAgent implements IChatAgent {
) { } ) { }
get id(): string { return this.data.id; } get id(): string { return this.data.id; }
get name(): string { return this.data.name ?? ''; }
get description(): string { return this.data.description ?? ''; }
get extensionId(): ExtensionIdentifier { return this.data.extensionId; } get extensionId(): ExtensionIdentifier { return this.data.extensionId; }
get isDefault(): boolean | undefined { return this.data.isDefault; } get isDefault(): boolean | undefined { return this.data.isDefault; }
get metadata(): IChatAgentMetadata { return this.data.metadata; } get metadata(): IChatAgentMetadata { return this.data.metadata; }

View file

@ -19,10 +19,6 @@ export interface IChatContributionService {
registerChatProvider(provider: IChatProviderContribution): void; registerChatProvider(provider: IChatProviderContribution): void;
deregisterChatProvider(providerId: string): void; deregisterChatProvider(providerId: string): void;
getViewIdForProvider(providerId: string): string; getViewIdForProvider(providerId: string): string;
readonly registeredParticipants: IChatParticipantContribution[];
registerChatParticipant(participant: IChatParticipantContribution): void;
deregisterChatParticipant(participant: IChatParticipantContribution): void;
} }
export interface IRawChatProviderContribution { export interface IRawChatProviderContribution {
@ -44,6 +40,7 @@ export interface IRawChatCommandContribution {
export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook'; export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook';
export interface IRawChatParticipantContribution { export interface IRawChatParticipantContribution {
id: string;
name: string; name: string;
description?: string; description?: string;
isDefault?: boolean; isDefault?: boolean;

View file

@ -591,7 +591,7 @@ export class ChatModel extends Disposable implements IChatModel {
const request = new ChatRequestModel(this, parsedRequest, variableData); const request = new ChatRequestModel(this, parsedRequest, variableData);
if (raw.response || raw.result || (raw as any).responseErrorDetails) { if (raw.response || raw.result || (raw as any).responseErrorDetails) {
const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format
revive<ISerializableChatAgentData>(raw.agent) : undefined; this.reviveSerializedAgent(raw.agent) : undefined;
// Port entries from old format // Port entries from old format
const result = 'responseErrorDetails' in raw ? const result = 'responseErrorDetails' in raw ?
@ -613,6 +613,16 @@ export class ChatModel extends Disposable implements IChatModel {
} }
} }
private reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData {
const agent = 'name' in raw ?
raw :
{
...(raw as any),
name: (raw as any).id,
};
return revive(agent);
}
private getParsedRequestFromString(message: string): IParsedChatRequest { private getParsedRequestFromString(message: string): IParsedChatRequest {
// TODO These offsets won't be used, but chat replies need to go through the parser as well // TODO These offsets won't be used, but chat replies need to go through the parser as well
const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)]; const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];
@ -703,7 +713,7 @@ export class ChatModel extends Disposable implements IChatModel {
} else if (progress.kind === 'usedContext' || progress.kind === 'reference') { } else if (progress.kind === 'usedContext' || progress.kind === 'reference') {
request.response.applyReference(progress); request.response.applyReference(progress);
} else if (progress.kind === 'agentDetection') { } else if (progress.kind === 'agentDetection') {
const agent = this.chatAgentService.getAgent(progress.agentName); const agent = this.chatAgentService.getAgent(progress.agentId);
if (agent) { if (agent) {
request.response.setAgent(agent, progress.command); request.response.setAgent(agent, progress.command);
} }
@ -802,7 +812,7 @@ export class ChatModel extends Disposable implements IChatModel {
vote: r.response?.vote, vote: r.response?.vote,
agent: r.response?.agent ? agent: r.response?.agent ?
// May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here.
{ id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault } { id: r.response.agent.id, name: r.response.agent.name, description: r.response.agent.description, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], locations: r.response.agent.locations, isDefault: r.response.agent.isDefault }
: undefined, : undefined,
slashCommand: r.response?.slashCommand, slashCommand: r.response?.slashCommand,
usedContext: r.response?.usedContext, usedContext: r.response?.usedContext,

View file

@ -75,7 +75,7 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart {
constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { }
get text(): string { get text(): string {
return `${chatAgentLeader}${this.agent.id}`; return `${chatAgentLeader}${this.agent.name}`;
} }
get promptText(): string { get promptText(): string {
@ -92,6 +92,8 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart {
editorRange: this.editorRange, editorRange: this.editorRange,
agent: { agent: {
id: this.agent.id, id: this.agent.id,
name: this.agent.name,
description: this.agent.description,
metadata: this.agent.metadata metadata: this.agent.metadata
} }
}; };
@ -167,10 +169,19 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed
(part as ChatRequestVariablePart).variableArg (part as ChatRequestVariablePart).variableArg
); );
} else if (part.kind === ChatRequestAgentPart.Kind) { } else if (part.kind === ChatRequestAgentPart.Kind) {
let agent = (part as ChatRequestAgentPart).agent;
if (!('name' in agent)) {
// Port old format
agent = {
...(agent as any),
name: (agent as any).id
};
}
return new ChatRequestAgentPart( return new ChatRequestAgentPart(
new OffsetRange(part.range.start, part.range.endExclusive), new OffsetRange(part.range.start, part.range.endExclusive),
part.editorRange, part.editorRange,
(part as ChatRequestAgentPart).agent agent
); );
} else if (part.kind === ChatRequestAgentSubcommandPart.Kind) { } else if (part.kind === ChatRequestAgentSubcommandPart.Kind) {
return new ChatRequestAgentSubcommandPart( return new ChatRequestAgentSubcommandPart(

View file

@ -6,10 +6,8 @@
import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { IPosition, Position } from 'vs/editor/common/core/position'; import { IPosition, Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables';
@ -17,18 +15,21 @@ const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent
const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)
const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command
export interface IChatParserContext {
/** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */
selectedAgent?: IChatAgentData;
}
export class ChatRequestParser { export class ChatRequestParser {
constructor( constructor(
@IChatAgentService private readonly agentService: IChatAgentService, @IChatAgentService private readonly agentService: IChatAgentService,
@IChatVariablesService private readonly variableService: IChatVariablesService, @IChatVariablesService private readonly variableService: IChatVariablesService,
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService, @IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService
@IChatService private readonly chatService: IChatService
) { } ) { }
parseChatRequest(sessionId: string, message: string): IParsedChatRequest { parseChatRequest(sessionId: string, message: string, context?: IChatParserContext): IParsedChatRequest {
const parts: IParsedChatRequestPart[] = []; const parts: IParsedChatRequestPart[] = [];
const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls
const model = this.chatService.getSession(sessionId)!;
let lineNumber = 1; let lineNumber = 1;
let column = 1; let column = 1;
@ -40,9 +41,9 @@ export class ChatRequestParser {
if (char === chatVariableLeader) { if (char === chatVariableLeader) {
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts); newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts);
} else if (char === chatAgentLeader) { } else if (char === chatAgentLeader) {
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts); newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, context);
} else if (char === chatSubcommandLeader) { } else if (char === chatSubcommandLeader) {
newPart = this.tryToParseSlashCommand(model, message.slice(i), message, i, new Position(lineNumber, column), parts); newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts);
} }
if (!newPart) { if (!newPart) {
@ -89,17 +90,23 @@ export class ChatRequestParser {
}; };
} }
private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined { private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
const nextVariableMatch = message.match(agentReg); const nextAgentMatch = message.match(agentReg);
if (!nextVariableMatch) { if (!nextAgentMatch) {
return; return;
} }
const [full, name] = nextVariableMatch; const [full, name] = nextAgentMatch;
const varRange = new OffsetRange(offset, offset + full.length); const agentRange = new OffsetRange(offset, offset + full.length);
const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
const agent = this.agentService.getAgent(name); const agents = this.agentService.getAgentsByName(name);
// If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the
// context and we use that one. Otherwise just pick the first.
const agent = agents.length > 1 && context?.selectedAgent ?
context.selectedAgent :
agents[0];
if (!agent) { if (!agent) {
return; return;
} }
@ -121,7 +128,7 @@ export class ChatRequestParser {
return; return;
} }
return new ChatRequestAgentPart(varRange, varEditorRange, agent); return new ChatRequestAgentPart(agentRange, agentEditorRange, agent);
} }
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined { private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
@ -142,7 +149,7 @@ export class ChatRequestParser {
return; return;
} }
private tryToParseSlashCommand(model: IChatModel, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined {
const nextSlashMatch = remainingMessage.match(slashReg); const nextSlashMatch = remainingMessage.match(slashReg);
if (!nextSlashMatch) { if (!nextSlashMatch) {
return; return;

View file

@ -15,6 +15,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
export interface IChat { export interface IChat {
@ -85,7 +86,7 @@ export interface IChatContentInlineReference {
} }
export interface IChatAgentDetection { export interface IChatAgentDetection {
agentName: string; agentId: string;
command?: IChatAgentCommand; command?: IChatAgentCommand;
kind: 'agentDetection'; kind: 'agentDetection';
} }
@ -283,7 +284,7 @@ export interface IChatService {
/** /**
* Returns whether the request was accepted. * Returns whether the request was accepted.
*/ */
sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean): Promise<IChatSendRequestData | undefined>; sendRequest(sessionId: string, message: string, implicitVariablesEnabled?: boolean, parserContext?: IChatParserContext): Promise<IChatSendRequestData | undefined>;
removeRequest(sessionid: string, requestId: string): Promise<void>; removeRequest(sessionid: string, requestId: string): Promise<void>;
cancelCurrentRequestForSession(sessionId: string): void; cancelCurrentRequestForSession(sessionId: string): void;
clearSession(sessionId: string): void; clearSession(sessionId: string): void;

View file

@ -25,7 +25,7 @@ import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult, IChatAgentServi
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatRequestParser, IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
@ -435,7 +435,7 @@ export class ChatService extends Disposable implements IChatService {
return this._startSession(data.providerId, data, CancellationToken.None); return this._startSession(data.providerId, data, CancellationToken.None);
} }
async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean): Promise<IChatSendRequestData | undefined> { async sendRequest(sessionId: string, request: string, implicitVariablesEnabled?: boolean, parserContext?: IChatParserContext): Promise<IChatSendRequestData | undefined> {
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);
if (!request.trim()) { if (!request.trim()) {
this.trace('sendRequest', 'Rejected empty message'); this.trace('sendRequest', 'Rejected empty message');
@ -458,7 +458,7 @@ export class ChatService extends Disposable implements IChatService {
return; return;
} }
const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request); const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, parserContext);
const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? this.chatAgentService.getDefaultAgent()!; const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? this.chatAgentService.getDefaultAgent()!;
const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);

View file

@ -33,7 +33,7 @@ suite('ChatVariables', function () {
instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatVariablesService, service); instantiationService.stub(IChatVariablesService, service);
instantiationService.stub(IChatService, new MockChatService()); instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
}); });
test('ChatVariables - resolveVariables', async function () { test('ChatVariables - resolveVariables', async function () {

View file

@ -27,6 +27,12 @@
}, },
agent: { agent: {
id: "agent", id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" }, metadata: { description: "" },
slashCommands: [ slashCommands: [
{ {

View file

@ -27,6 +27,12 @@
}, },
agent: { agent: {
id: "agent", id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" }, metadata: { description: "" },
slashCommands: [ slashCommands: [
{ {

View file

@ -13,6 +13,12 @@
}, },
agent: { agent: {
id: "agent", id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" }, metadata: { description: "" },
slashCommands: [ slashCommands: [
{ {

View file

@ -13,6 +13,12 @@
}, },
agent: { agent: {
id: "agent", id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" }, metadata: { description: "" },
slashCommands: [ slashCommands: [
{ {

View file

@ -13,6 +13,12 @@
}, },
agent: { agent: {
id: "agent", id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" }, metadata: { description: "" },
slashCommands: [ slashCommands: [
{ {

View file

@ -13,6 +13,12 @@
}, },
agent: { agent: {
id: "agent", id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" }, metadata: { description: "" },
slashCommands: [ slashCommands: [
{ {

View file

@ -13,6 +13,12 @@
}, },
agent: { agent: {
id: "agent", id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" }, metadata: { description: "" },
slashCommands: [ slashCommands: [
{ {

View file

@ -0,0 +1,96 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [
{
message: {
text: "@ChatProviderWithUsedContext test request",
parts: [
{
kind: "agent",
range: {
start: 0,
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 29
},
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
metadata: { }
}
},
{
range: {
start: 28,
endExclusive: 41
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 42
},
text: " test request",
kind: "text"
}
]
},
variableData: { variables: [ ] },
response: [ ],
result: { metadata: { metadataKey: "value" } },
followups: undefined,
isCanceled: false,
vote: undefined,
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
metadata: { },
slashCommands: [ ],
locations: [ 1 ],
isDefault: undefined
},
slashCommand: undefined,
usedContext: {
documents: [
{
uri: {
scheme: "file",
authority: "",
path: "/test/path/to/file",
query: "",
fragment: "",
_formatted: null,
_fsPath: null
},
version: 3,
ranges: [
{
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 2
}
]
}
],
kind: "usedContext"
},
contentReferences: [ ]
}
],
providerId: "testProvider"
}

View file

@ -0,0 +1,9 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [ ],
providerId: "testProvider"
}

View file

@ -0,0 +1,102 @@
{
requesterUsername: "test",
requesterAvatarIconUri: undefined,
responderUsername: "test",
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [
{
message: {
parts: [
{
kind: "agent",
range: {
start: 0,
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 29
},
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
metadata: {
requester: { name: "test" },
fullName: "test"
}
}
},
{
range: {
start: 28,
endExclusive: 41
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 42
},
text: " test request",
kind: "text"
}
],
text: "@ChatProviderWithUsedContext test request"
},
variableData: { variables: [ ] },
response: [ ],
result: { metadata: { metadataKey: "value" } },
followups: undefined,
isCanceled: false,
vote: undefined,
agent: {
id: "ChatProviderWithUsedContext",
name: "ChatProviderWithUsedContext",
description: undefined,
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
metadata: {
requester: { name: "test" },
fullName: "test"
},
slashCommands: [ ],
locations: [ 1 ],
isDefault: undefined
},
slashCommand: undefined,
usedContext: {
documents: [
{
uri: {
scheme: "file",
authority: "",
path: "/test/path/to/file",
query: "",
fragment: "",
_formatted: null,
_fsPath: null
},
version: 3,
ranges: [
{
startLineNumber: 1,
startColumn: 1,
endLineNumber: 2,
endColumn: 2
}
]
}
],
kind: "usedContext"
},
contentReferences: [ ]
}
],
providerId: "testProvider"
}

View file

@ -30,7 +30,7 @@ suite('ChatModel', () => {
instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));
instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
}); });
test('Waits for initialization', async () => { test('Waits for initialization', async () => {

View file

@ -15,7 +15,7 @@ import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
suite('ChatRequestParser', () => { suite('ChatRequestParser', () => {
@ -31,7 +31,7 @@ suite('ChatRequestParser', () => {
instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatService, new MockChatService()); instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
varService = mockObject<IChatVariablesService>()({}); varService = mockObject<IChatVariablesService>()({});
varService.getDynamicVariables.returns([]); varService.getDynamicVariables.returns([]);
@ -112,12 +112,12 @@ suite('ChatRequestParser', () => {
}); });
const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => {
return <IChatAgentData>{ id: 'agent', metadata: { description: '' }, slashCommands }; return <IChatAgentData>{ id: 'agent', name: 'agent', extensionId: nullExtensionDescription.identifier, locations: [], metadata: { description: '' }, slashCommands };
}; };
test('agent with subcommand after text', async () => { test('agent with subcommand after text', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser); parser = instantiationService.createInstance(ChatRequestParser);
@ -127,7 +127,7 @@ suite('ChatRequestParser', () => {
test('agents, subCommand', async () => { test('agents, subCommand', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser); parser = instantiationService.createInstance(ChatRequestParser);
@ -137,7 +137,7 @@ suite('ChatRequestParser', () => {
test('agent with question mark', async () => { test('agent with question mark', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser); parser = instantiationService.createInstance(ChatRequestParser);
@ -147,7 +147,7 @@ suite('ChatRequestParser', () => {
test('agent and subcommand with leading whitespace', async () => { test('agent and subcommand with leading whitespace', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser); parser = instantiationService.createInstance(ChatRequestParser);
@ -157,7 +157,7 @@ suite('ChatRequestParser', () => {
test('agent and subcommand after newline', async () => { test('agent and subcommand after newline', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser); parser = instantiationService.createInstance(ChatRequestParser);
@ -167,7 +167,7 @@ suite('ChatRequestParser', () => {
test('agent not first', async () => { test('agent not first', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser); parser = instantiationService.createInstance(ChatRequestParser);
@ -177,7 +177,7 @@ suite('ChatRequestParser', () => {
test('agents and variables and multiline', async () => { test('agents and variables and multiline', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
varService.hasVariable.returns(true); varService.hasVariable.returns(true);
@ -189,7 +189,7 @@ suite('ChatRequestParser', () => {
test('agents and variables and multiline, part2', async () => { test('agents and variables and multiline, part2', async () => {
const agentsService = mockObject<IChatAgentService>()({}); const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any); instantiationService.stub(IChatAgentService, agentsService as any);
varService.hasVariable.returns(true); varService.hasVariable.returns(true);

View file

@ -20,7 +20,6 @@ import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { ChatAgentLocation, ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatAgentLocation, ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
@ -28,11 +27,11 @@ import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChat
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService';
import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables'; import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/mockChatVariables';
import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService';
import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService';
class SimpleTestProvider extends Disposable implements IChatProvider { class SimpleTestProvider extends Disposable implements IChatProvider {
private static sessionId = 0; private static sessionId = 0;
@ -57,6 +56,7 @@ class SimpleTestProvider extends Disposable implements IChatProvider {
const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext';
const chatAgentWithUsedContext: IChatAgent = { const chatAgentWithUsedContext: IChatAgent = {
id: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId,
name: chatAgentWithUsedContextId,
extensionId: nullExtensionDescription.identifier, extensionId: nullExtensionDescription.identifier,
locations: [ChatAgentLocation.Panel], locations: [ChatAgentLocation.Panel],
metadata: {}, metadata: {},
@ -82,7 +82,7 @@ const chatAgentWithUsedContext: IChatAgent = {
}, },
}; };
suite('Chat', () => { suite('ChatService', () => {
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
let storageService: IStorageService; let storageService: IStorageService;
@ -104,13 +104,8 @@ suite('Chat', () => {
instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)));
instantiationService.stub(IChatService, new MockChatService()); instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatContributionService, new MockChatContributionService(
[
{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true },
{ extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId },
]));
chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); chatAgentService = instantiationService.createInstance(ChatAgentService);
instantiationService.stub(IChatAgentService, chatAgentService); instantiationService.stub(IChatAgentService, chatAgentService);
const agent = { const agent = {
@ -118,7 +113,9 @@ suite('Chat', () => {
return {}; return {};
}, },
} satisfies IChatAgentImplementation; } satisfies IChatAgentImplementation;
testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); testDisposables.add(chatAgentService.registerAgent('testAgent', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] }));
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] }));
testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent));
chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' });
}); });
@ -209,7 +206,7 @@ suite('Chat', () => {
}); });
test('can serialize', async () => { test('can serialize', async () => {
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));
chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' });
const testService = testDisposables.add(instantiationService.createInstance(ChatService)); const testService = testDisposables.add(instantiationService.createInstance(ChatService));
testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider'))));
@ -230,7 +227,7 @@ suite('Chat', () => {
test('can deserialize', async () => { test('can deserialize', async () => {
let serializedChatData: ISerializableChatData; let serializedChatData: ISerializableChatData;
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));
// create the first service, send request, get response, and serialize the state // create the first service, send request, get response, and serialize the state
{ // serapate block to not leak variables in outer scope { // serapate block to not leak variables in outer scope

View file

@ -9,7 +9,6 @@ export class MockChatContributionService implements IChatContributionService {
_serviceBrand: undefined; _serviceBrand: undefined;
constructor( constructor(
public readonly registeredParticipants: IChatParticipantContribution[] = []
) { } ) { }
registeredProviders: IChatProviderContribution[] = []; registeredProviders: IChatProviderContribution[] = [];

View file

@ -28,7 +28,10 @@ suite('VoiceChat', () => {
extensionId: ExtensionIdentifier = nullExtensionDescription.identifier; extensionId: ExtensionIdentifier = nullExtensionDescription.identifier;
locations: ChatAgentLocation[] = [ChatAgentLocation.Panel]; locations: ChatAgentLocation[] = [ChatAgentLocation.Panel];
constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { } public readonly name: string;
constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) {
this.name = id;
}
invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error('Method not implemented.'); } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error('Method not implemented.'); }
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); }
metadata = {}; metadata = {};
@ -47,17 +50,18 @@ suite('VoiceChat', () => {
class TestChatAgentService implements IChatAgentService { class TestChatAgentService implements IChatAgentService {
_serviceBrand: undefined; _serviceBrand: undefined;
readonly onDidChangeAgents = Event.None; readonly onDidChangeAgents = Event.None;
registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); }
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); }
invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error(); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error(); }
getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); } getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); }
getRegisteredAgents(): Array<IChatAgent> { return agents; }
getActivatedAgents(): IChatAgent[] { return agents; } getActivatedAgents(): IChatAgent[] { return agents; }
getAgents(): IChatAgent[] { return agents; } getAgents(): IChatAgent[] { return agents; }
getAgent(id: string): IChatAgent | undefined { throw new Error(); }
getDefaultAgent(): IChatAgent | undefined { throw new Error(); } getDefaultAgent(): IChatAgent | undefined { throw new Error(); }
getSecondaryAgent(): IChatAgent | undefined { throw new Error(); } getSecondaryAgent(): IChatAgent | undefined { throw new Error(); }
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error(); } registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); }
getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }
getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); }
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); }
} }
class TestSpeechService implements ISpeechService { class TestSpeechService implements ISpeechService {

View file

@ -7,16 +7,20 @@ import * as assert from 'assert';
import { equals } from 'vs/base/common/arrays'; import { equals } from 'vs/base/common/arrays';
import { timeout } from 'vs/base/common/async'; import { timeout } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { mock } from 'vs/base/test/common/mock'; import { mock } from 'vs/base/test/common/mock';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService'; import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model'; import { ITextModel } from 'vs/editor/common/model';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { IModelService } from 'vs/editor/common/services/model'; import { IModelService } from 'vs/editor/common/services/model';
import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService';
import { instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { instantiateTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
@ -30,24 +34,17 @@ import { IViewDescriptorService } from 'vs/workbench/common/views';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
import { IInlineChatSavingService } from '../../browser/inlineChatSavingService';
import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl';
import { IInlineChatSessionService } from '../../browser/inlineChatSessionService';
import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatEditResponse, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatEditResponse, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl';
import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { EditOperation } from 'vs/editor/common/core/editOperation'; import { IInlineChatSavingService } from '../../browser/inlineChatSavingService';
import { IInlineChatSessionService } from '../../browser/inlineChatSessionService';
import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl';
import { TestWorkerService } from './testWorkerService'; import { TestWorkerService } from './testWorkerService';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { Schemas } from 'vs/base/common/network';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService';
import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
suite('InteractiveChatController', function () { suite('InteractiveChatController', function () {
class TestController extends InlineChatController { class TestController extends InlineChatController {
@ -117,8 +114,6 @@ suite('InteractiveChatController', function () {
const serviceCollection = new ServiceCollection( const serviceCollection = new ServiceCollection(
[IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)],
[IContextKeyService, contextKeyService], [IContextKeyService, contextKeyService],
[IChatContributionService, new MockChatContributionService(
[{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])],
[IChatAgentService, new SyncDescriptor(ChatAgentService)], [IChatAgentService, new SyncDescriptor(ChatAgentService)],
[IInlineChatService, inlineChatService], [IInlineChatService, inlineChatService],
[IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)],
@ -152,15 +147,7 @@ suite('InteractiveChatController', function () {
}] }]
); );
instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection));
const chatAgentService = instaService.get(IChatAgentService);
const agent = {
async invoke(request, progress, history, token) {
return {};
},
} satisfies IChatAgentImplementation;
store.add(chatAgentService.registerAgent('testAgent', agent));
inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService));
model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null));

View file

@ -14,14 +14,18 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
import { changeCellToKind, computeCellLinesContents, copyCellRange, joinCellsWithSurrounds, joinSelectedCells, moveCellRange } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { changeCellToKind, computeCellLinesContents, copyCellRange, joinCellsWithSurrounds, joinSelectedCells, moveCellRange } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { cellExecutionArgs, CellOverflowToolbarGroups, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookCellAction, NotebookMultiCellAction, parseMultiCellExecutionArgs, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellOutputViewModel, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellOutputViewModel, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons';
import { CellEditType, CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellEditType, CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { Range } from 'vs/editor/common/core/range';
import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController';
import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
//#region Move/Copy cells //#region Move/Copy cells
const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp'; const MOVE_CELL_UP_COMMAND_ID = 'notebook.cell.moveUp';
@ -353,6 +357,7 @@ const COLLAPSE_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.collapseAllCellOutpu
const EXPAND_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.expandAllCellOutputs'; const EXPAND_ALL_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.expandAllCellOutputs';
const TOGGLE_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.toggleOutputs'; const TOGGLE_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.toggleOutputs';
const TOGGLE_CELL_OUTPUT_SCROLLING = 'notebook.cell.toggleOutputScrolling'; const TOGGLE_CELL_OUTPUT_SCROLLING = 'notebook.cell.toggleOutputScrolling';
const OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID = 'notebook.cell.openFailureActions';
registerAction2(class CollapseCellInputAction extends NotebookMultiCellAction { registerAction2(class CollapseCellInputAction extends NotebookMultiCellAction {
constructor() { constructor() {
@ -579,6 +584,45 @@ registerAction2(class ToggleCellOutputScrolling extends NotebookMultiCellAction
} }
}); });
registerAction2(class ExpandAllCellOutputsAction extends NotebookCellAction {
constructor() {
super({
id: OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID,
title: localize2('notebookActions.cellFailureActions', "Show Cell Failure Actions"),
precondition: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()),
f1: true,
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS, NOTEBOOK_CELL_EDITOR_FOCUSED.toNegated()),
primary: KeyMod.CtrlCmd | KeyCode.Period,
weight: KeybindingWeight.WorkbenchContrib
}
});
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> {
if (context.cell instanceof CodeCellViewModel) {
const error = context.cell.cellErrorDetails;
if (error?.location) {
const location = Range.lift({
startLineNumber: error.location.startLineNumber + 1,
startColumn: error.location.startColumn + 1,
endLineNumber: error.location.endLineNumber + 1,
endColumn: error.location.endColumn + 1
});
context.notebookEditor.setCellEditorSelection(context.cell, Range.lift(location));
const editor = findTargetCellEditor(context, context.cell);
if (editor) {
const controller = CodeActionController.get(editor);
controller?.manualTriggerAtCurrentPosition(
localize('cellCommands.quickFix.noneMessage', "No code actions available"),
CodeActionTriggerSource.Default,
{ include: CodeActionKind.QuickFix });
}
}
}
}
});
//#endregion //#endregion
function forEachCell(editor: INotebookEditor, callback: (cell: ICellViewModel, index: number) => void) { function forEachCell(editor: INotebookEditor, callback: (cell: ICellViewModel, index: number) => void) {

View file

@ -70,7 +70,9 @@ export class CellDiagnostics extends Disposable {
} }
public clear() { public clear() {
this.clearDiagnostics(); if (this.ErrorDetails) {
this.clearDiagnostics();
}
} }
private clearDiagnostics() { private clearDiagnostics() {

View file

@ -13,7 +13,7 @@ import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cell
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_GENERATED_BY_CHAT, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
export class CellContextKeyPart extends CellContentPart { export class CellContextKeyPart extends CellContentPart {
@ -47,6 +47,7 @@ export class CellContextKeyManager extends Disposable {
private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>;
private cellResource!: IContextKey<string>; private cellResource!: IContextKey<string>;
private cellGeneratedByChat!: IContextKey<boolean>; private cellGeneratedByChat!: IContextKey<boolean>;
private cellHasErrorDiagnostics!: IContextKey<boolean>;
private markdownEditMode!: IContextKey<boolean>; private markdownEditMode!: IContextKey<boolean>;
@ -74,6 +75,7 @@ export class CellContextKeyManager extends Disposable {
this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService);
this.cellGeneratedByChat = NOTEBOOK_CELL_GENERATED_BY_CHAT.bindTo(this._contextKeyService); this.cellGeneratedByChat = NOTEBOOK_CELL_GENERATED_BY_CHAT.bindTo(this._contextKeyService);
this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService); this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService);
this.cellHasErrorDiagnostics = NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS.bindTo(this._contextKeyService);
if (element) { if (element) {
this.updateForElement(element); this.updateForElement(element);
@ -200,6 +202,10 @@ export class CellContextKeyManager extends Disposable {
this.cellRunState.set('idle'); this.cellRunState.set('idle');
this.cellExecuting.set(false); this.cellExecuting.set(false);
} }
if (this.element instanceof CodeCellViewModel) {
this.cellHasErrorDiagnostics.set(!!this.element.cellErrorDetails);
}
} }
private updateForEditState() { private updateForEditState() {

View file

@ -47,6 +47,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
private _outputCollection: number[] = []; private _outputCollection: number[] = [];
private readonly _cellDiagnostics: CellDiagnostics; private readonly _cellDiagnostics: CellDiagnostics;
get cellErrorDetails() {
return this._cellDiagnostics.ErrorDetails;
}
private _outputsTop: PrefixSumComputer | null = null; private _outputsTop: PrefixSumComputer | null = null;
@ -171,7 +174,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
if (outputLayoutChange) { if (outputLayoutChange) {
this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs'); this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs');
} }
if (this._outputCollection.length === 0 && this._cellDiagnostics.ErrorDetails) { if (this._outputCollection.length === 0) {
this._cellDiagnostics.clear(); this._cellDiagnostics.clear();
} }
dispose(removedOutputs); dispose(removedOutputs);
@ -433,6 +436,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
protected onDidChangeTextModelContent(): void { protected onDidChangeTextModelContent(): void {
if (this.getEditState() !== CellEditState.Editing) { if (this.getEditState() !== CellEditState.Editing) {
this._cellDiagnostics.clear();
this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent'); this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent');
this._onDidChangeState.fire({ contentChanged: true }); this._onDidChangeState.fire({ contentChanged: true });
} }

View file

@ -47,6 +47,7 @@ export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey<boolean>('noteboo
export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey<boolean>('notebookCellOutputIsCollapsed', false); export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey<boolean>('notebookCellOutputIsCollapsed', false);
export const NOTEBOOK_CELL_RESOURCE = new RawContextKey<string>('notebookCellResource', ''); export const NOTEBOOK_CELL_RESOURCE = new RawContextKey<string>('notebookCellResource', '');
export const NOTEBOOK_CELL_GENERATED_BY_CHAT = new RawContextKey<boolean>('notebookCellGenerateByChat', false); export const NOTEBOOK_CELL_GENERATED_BY_CHAT = new RawContextKey<boolean>('notebookCellGenerateByChat', false);
export const NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS = new RawContextKey<boolean>('notebookCellHasErrorDiagnostics', false);
// Kernels // Kernels
export const NOTEBOOK_KERNEL = new RawContextKey<string>('notebookKernel', undefined); export const NOTEBOOK_KERNEL = new RawContextKey<string>('notebookKernel', undefined);

View file

@ -12,8 +12,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat'; import { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatAgentRequest, IChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatUserAction, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatUserAction, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
@ -81,7 +81,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr
readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store);
readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store);
private _terminalAgentId = 'terminal'; private _terminalAgentName = 'terminal';
private _terminalAgentId: string | undefined;
private _model: MutableDisposable<ChatModel> = this._register(new MutableDisposable()); private _model: MutableDisposable<ChatModel> = this._register(new MutableDisposable());
@ -97,7 +98,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr
@IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
@IChatService private readonly _chatService: IChatService, @IChatService private readonly _chatService: IChatService,
@IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService @IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService,
) { ) {
super(); super();
@ -113,14 +114,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr
return; return;
} }
if (!this._chatAgentService.getAgent(this._terminalAgentId)) { if (!this.initTerminalAgent()) {
this._register(this._chatAgentService.onDidChangeAgents(() => { this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent()));
if (this._chatAgentService.getAgent(this._terminalAgentId)) {
this._terminalAgentRegisteredContextKey.set(true);
}
}));
} else {
this._terminalAgentRegisteredContextKey.set(true);
} }
this._register(this._chatCodeBlockContextProviderService.registerProvider({ this._register(this._chatCodeBlockContextProviderService.registerProvider({
getCodeBlockContext: (editor) => { getCodeBlockContext: (editor) => {
@ -141,6 +136,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr
}, 'terminal')); }, 'terminal'));
} }
private initTerminalAgent(): boolean {
const terminalAgent = this._chatAgentService.getAgentsByName(this._terminalAgentName)[0];
if (terminalAgent) {
this._terminalAgentId = terminalAgent.id;
this._terminalAgentRegisteredContextKey.set(true);
return true;
}
return false;
}
xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void {
if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) {
return; return;
@ -285,13 +291,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr
const requestProps: IChatAgentRequest = { const requestProps: IChatAgentRequest = {
sessionId: model.sessionId, sessionId: model.sessionId,
requestId: this._currentRequest!.id, requestId: this._currentRequest!.id,
agentId: this._terminalAgentId, agentId: this._terminalAgentId!,
message: this._lastInput, message: this._lastInput,
variables: { variables: [] }, variables: { variables: [] },
location: ChatAgentLocation.Terminal location: ChatAgentLocation.Terminal
}; };
try { try {
const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken); const task = this._chatAgentService.invokeAgent(this._terminalAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model), cancellationToken);
this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined); this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined);
this._chatWidget?.value.inlineChatWidget.updateFollowUps(undefined); this._chatWidget?.value.inlineChatWidget.updateFollowUps(undefined);
this._chatWidget?.value.inlineChatWidget.updateProgress(true); this._chatWidget?.value.inlineChatWidget.updateProgress(true);

View file

@ -21,9 +21,9 @@ declare module 'vscode' {
readonly prompt: string; readonly prompt: string;
/** /**
* The name of the chat participant and contributing extension to which this request was directed. * The id of the chat participant and contributing extension to which this request was directed.
*/ */
readonly participant: { readonly extensionId: string; readonly name: string }; readonly participant: string;
/** /**
* The name of the {@link ChatCommand command} that was selected for this request. * The name of the {@link ChatCommand command} that was selected for this request.
@ -35,7 +35,7 @@ declare module 'vscode' {
*/ */
readonly variables: ChatResolvedVariable[]; readonly variables: ChatResolvedVariable[];
private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: { extensionId: string; name: string }); private constructor(prompt: string, command: string | undefined, variables: ChatResolvedVariable[], participant: string);
} }
/** /**
@ -54,16 +54,16 @@ declare module 'vscode' {
readonly result: ChatResult; readonly result: ChatResult;
/** /**
* The name of the chat participant and contributing extension that this response came from. * The id of the chat participant and contributing extension that this response came from.
*/ */
readonly participant: { readonly extensionId: string; readonly name: string }; readonly participant: string;
/** /**
* The name of the command that this response came from. * The name of the command that this response came from.
*/ */
readonly command?: string; readonly command?: string;
private constructor(response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart>, result: ChatResult, participant: { extensionId: string; name: string }); private constructor(response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart>, result: ChatResult, participant: string);
} }
export interface ChatContext { export interface ChatContext {
@ -158,7 +158,7 @@ declare module 'vscode' {
label?: string; label?: string;
/** /**
* By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant. * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID.
* Followups can only invoke a participant that was contributed by the same extension. * Followups can only invoke a participant that was contributed by the same extension.
*/ */
participant?: string; participant?: string;
@ -192,9 +192,9 @@ declare module 'vscode' {
*/ */
export interface ChatParticipant { export interface ChatParticipant {
/** /**
* The short name by which this participant is referred to in the UI, e.g `workspace`. * A unique ID for this participant.
*/ */
readonly name: string; readonly id: string;
/** /**
* Icon for the participant shown in UI. * Icon for the participant shown in UI.
@ -446,12 +446,11 @@ declare module 'vscode' {
/** /**
* Create a new {@link ChatParticipant chat participant} instance. * Create a new {@link ChatParticipant chat participant} instance.
* *
* @param name Short name by which the participant is referred to in the UI. The name must be unique for the extension * @param id A unique identifier for the participant.
* contributing the participant but can collide with names from other extensions.
* @param handler A request handler for the participant. * @param handler A request handler for the participant.
* @returns A new chat participant * @returns A new chat participant
*/ */
export function createChatParticipant(name: string, handler: ChatRequestHandler): ChatParticipant; export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant;
} }
/** /**

View file

@ -173,7 +173,9 @@ declare module 'vscode' {
/** /**
* Create a chat participant with the extended progress type * Create a chat participant with the extended progress type
*/ */
export function createChatParticipant(name: string, handler: ChatExtendedRequestHandler): ChatParticipant; export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant;
export function createDynamicChatParticipant(id: string, name: string, description: string, handler: ChatExtendedRequestHandler): ChatParticipant;
} }
/* /*
@ -280,12 +282,4 @@ declare module 'vscode' {
*/ */
resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult<ChatVariableValue[]>; resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult<ChatVariableValue[]>;
} }
export interface ChatParticipant {
/**
* A human-readable description explaining what this participant does.
* Only allow a static description for normal participants. Here where dynamic participants are allowed, the description must be able to be set as well.
*/
description?: string;
}
} }