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.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.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.never": "Never create Markdown links.",
"configuration.markdown.editor.drop.enabled.always": "Always insert 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.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.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.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.always": "Always insert 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.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.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.",
"configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.",

View file

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

View file

@ -30,7 +30,7 @@ suite('chat', () => {
function setupParticipant(): Event<{ request: ChatRequest; context: ChatContext }> {
const emitter = new EventEmitter<{ request: ChatRequest; context: ChatContext }>();
disposables.push();
disposables.push(emitter);
disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_token: CancellationToken): ProviderResult<InteractiveSession> => {
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 });
return null;
});
@ -49,12 +49,12 @@ suite('chat', () => {
return emitter.event;
}
test('participant and slash command', async () => {
test('participant and slash command history', async () => {
const onRequest = setupParticipant();
commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' });
let i = 0;
onRequest(request => {
disposables.push(onRequest(request => {
if (i === 0) {
assert.deepStrictEqual(request.request.command, 'hello');
assert.strictEqual(request.request.prompt, 'friend');
@ -62,10 +62,10 @@ suite('chat', () => {
commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' });
} else {
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');
}
});
}));
});
test('participant and variable', async () => {
@ -93,7 +93,7 @@ suite('chat', () => {
}));
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' } };
});
participant.isDefault = true;

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.88.0",
"distro": "ff3bff60edcc6e1f7269509e1673036c00fa62bd",
"distro": "7734ec27c8ec09ddc68bc3618e17d9a8b40fbfd9",
"author": {
"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;
}
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 {
this.input.removeAttribute('disabled');
}

View file

@ -237,7 +237,7 @@ export class Checkbox extends Widget {
constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) {
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;

View file

@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart';
import { Position } from 'vs/editor/common/core/position';
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
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 { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import { EditorTheme } from 'vs/editor/common/editorTheme';
@ -29,7 +29,9 @@ class Settings {
public readonly borderColor: string | null;
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 backgroundColor: Color | null;
@ -55,8 +57,12 @@ class Settings {
this.borderColor = borderColor ? borderColor.toString() : null;
this.hideCursor = options.get(EditorOption.hideCursorInOverviewRuler);
const cursorColor = theme.getColor(editorCursorForeground);
this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null;
const cursorColorSingle = theme.getColor(editorCursorForeground);
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;
@ -189,7 +195,9 @@ class Settings {
&& this.renderBorder === other.renderBorder
&& this.borderColor === other.borderColor
&& this.hideCursor === other.hideCursor
&& this.cursorColor === other.cursorColor
&& this.cursorColorSingle === other.cursorColorSingle
&& this.cursorColorPrimary === other.cursorColorPrimary
&& this.cursorColorSecondary === other.cursorColorSecondary
&& this.themeType === other.themeType
&& Color.equals(this.backgroundColor, other.backgroundColor)
&& this.top === other.top
@ -213,6 +221,11 @@ const enum OverviewRulerLane {
Full = 7
}
type Cursor = {
position: Position;
color: string | null;
};
const enum ShouldRenderValue {
NotNeeded = 0,
Maybe = 1,
@ -226,10 +239,10 @@ export class DecorationsOverviewRuler extends ViewPart {
private readonly _tokensColorTrackerListener: IDisposable;
private readonly _domNode: FastDomNode<HTMLCanvasElement>;
private _settings!: Settings;
private _cursorPositions: Position[];
private _cursorPositions: Cursor[];
private _renderedDecorations: OverviewRulerDecorationsGroup[] = [];
private _renderedCursorPositions: Position[] = [];
private _renderedCursorPositions: Cursor[] = [];
constructor(context: ViewContext) {
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 {
@ -298,9 +311,13 @@ export class DecorationsOverviewRuler extends ViewPart {
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
this._cursorPositions = [];
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();
}
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)) {
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;
}
if (this._actualShouldRender === ShouldRenderValue.Maybe) {
@ -443,17 +460,21 @@ export class DecorationsOverviewRuler extends ViewPart {
}
// Draw cursors
if (!this._settings.hideCursor && this._settings.cursorColor) {
if (!this._settings.hideCursor) {
const cursorHeight = (2 * this._settings.pixelRatio) | 0;
const halfCursorHeight = (cursorHeight / 2) | 0;
const cursorX = this._settings.x[OverviewRulerLane.Full];
const cursorW = this._settings.w[OverviewRulerLane.Full];
canvasCtx.fillStyle = this._settings.cursorColor;
let prevY1 = -100;
let prevY2 = -100;
let prevColor: string | null = null;
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;
if (yCenter < halfCursorHeight) {
@ -464,9 +485,9 @@ export class DecorationsOverviewRuler extends ViewPart {
const y1 = yCenter - halfCursorHeight;
const y2 = y1 + cursorHeight;
if (y1 > prevY2 + 1) {
if (y1 > prevY2 + 1 || color !== prevColor) {
// flush prev
if (i !== 0) {
if (i !== 0 && prevColor) {
canvasCtx.fillRect(cursorX, prevY1, cursorW, prevY2 - prevY1);
}
prevY1 = y1;
@ -477,8 +498,12 @@ export class DecorationsOverviewRuler extends ViewPart {
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) {

View file

@ -35,6 +35,12 @@ class ViewCursorRenderData {
) { }
}
export enum CursorPlurality {
Single,
MultiPrimary,
MultiSecondary,
}
export class ViewCursor {
private readonly _context: ViewContext;
private readonly _domNode: FastDomNode<HTMLElement>;
@ -47,11 +53,12 @@ export class ViewCursor {
private _isVisible: boolean;
private _position: Position;
private _pluralityClass: string;
private _lastRenderedContent: string;
private _renderData: ViewCursorRenderData | null;
constructor(context: ViewContext) {
constructor(context: ViewContext, plurality: CursorPlurality) {
this._context = context;
const options = this._context.configuration.options;
const fontInfo = options.get(EditorOption.fontInfo);
@ -73,6 +80,8 @@ export class ViewCursor {
this._domNode.setDisplay('none');
this._position = new Position(1, 1);
this._pluralityClass = '';
this.setPlurality(plurality);
this._lastRenderedContent = '';
this._renderData = null;
@ -86,6 +95,23 @@ export class ViewCursor {
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 {
if (!this._isVisible) {
this._domNode.setVisibility('inherit');
@ -229,7 +255,7 @@ export class ViewCursor {
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.setTop(this._renderData.top);

View file

@ -7,10 +7,14 @@ import 'vs/css!./viewCursors';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
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 { 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 { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import * as viewEvents from 'vs/editor/common/viewEvents';
@ -57,7 +61,7 @@ export class ViewCursors extends ViewPart {
this._isVisible = false;
this._primaryCursor = new ViewCursor(this._context);
this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single);
this._secondaryCursors = [];
this._renderData = [];
@ -88,6 +92,7 @@ export class ViewCursors extends ViewPart {
}
// --- begin event handlers
public override onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean {
this._isComposingInput = true;
this._updateBlinking();
@ -120,6 +125,7 @@ export class ViewCursors extends ViewPart {
this._secondaryCursors.length !== secondaryPositions.length
|| (this._cursorSmoothCaretAnimation === 'explicit' && reason !== CursorChangeReason.Explicit)
);
this._primaryCursor.setPlurality(secondaryPositions.length ? CursorPlurality.MultiPrimary : CursorPlurality.Single);
this._primaryCursor.onCursorPositionChanged(position, pauseAnimation);
this._updateBlinking();
@ -127,7 +133,7 @@ export class ViewCursors extends ViewPart {
// Create new cursors
const addCnt = secondaryPositions.length - this._secondaryCursors.length;
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._secondaryCursors.push(newCursor);
}
@ -160,7 +166,6 @@ export class ViewCursors extends ViewPart {
return true;
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
// true for inline decorations that can end up relayouting text
return true;
@ -263,6 +268,7 @@ export class ViewCursors extends ViewPart {
}
}
}
// --- end blinking logic
private _updateDomClassName(): void {
@ -375,16 +381,29 @@ export class ViewCursors extends ViewPart {
}
registerThemingParticipant((theme, collector) => {
const caret = theme.getColor(editorCursorForeground);
if (caret) {
let caretBackground = theme.getColor(editorCursorBackground);
if (!caretBackground) {
caretBackground = caret.opposite();
}
collector.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
if (isHighContrast(theme.type)) {
collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
type CursorTheme = {
foreground: string;
background: string;
class: string;
};
const cursorThemes: CursorTheme[] = [
{ class: '.cursor', foreground: editorCursorForeground, background: editorCursorBackground },
{ 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 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 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 { isHighContrast } from 'vs/platform/theme/common/theme';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from '../common/types';
import { CodeActionModel, CodeActionsState } from './codeActionModel';
import { CodeActionAutoApply, CodeActionFilter, CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel';
import { HierarchicalKind } from 'vs/base/common/hierarchicalKind';
interface IActionShowOptions {
@ -291,7 +292,22 @@ export class CodeActionController extends Disposable implements IEditorContribut
if (token.isCancellationRequested) {
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) => {
if (action && action.action) {

View file

@ -545,9 +545,10 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => {
// 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, {});
disposables.add(model);
@ -562,11 +563,53 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => {
'const add1 = (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');
assert.strictEqual(model.getValue(), [
'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'));
});
@ -632,6 +675,7 @@ suite('`Full` Auto Indent On Type - TypeScript/JavaScript', () => {
test.skip('issue #43244: incorrect indentation', () => {
// https://github.com/microsoft/vscode/issues/43244
// potential regex to fix: "^.*[if|while|for]\s*\(.*\)\s*",
const model = createTextModel([
'function f() {',

View file

@ -237,9 +237,9 @@ export class LinkDetector extends Disposable implements IEditorContribution {
const fsPath = resources.originalFSPath(parsedUri);
let relativePath: string | null = null;
if (fsPath.startsWith('/./')) {
if (fsPath.startsWith('/./') || fsPath.startsWith('\\.\\')) {
relativePath = `.${fsPath.substr(1)}`;
} else if (fsPath.startsWith('//./')) {
} else if (fsPath.startsWith('//./') || fsPath.startsWith('\\\\.\\')) {
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 { Registry } from 'vs/platform/registry/common/platform';
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 {
@ -138,7 +138,7 @@ class RenameController implements IEditorContribution {
return editor.getContribution<RenameController>(RenameController.ID);
}
private readonly _renameInputField: RenameInputField;
private readonly _renameInputField: RenameWidget;
private readonly _disposableStore = new DisposableStore();
private _cts: CancellationTokenSource = new CancellationTokenSource();
@ -153,7 +153,7 @@ class RenameController implements IEditorContribution {
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@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 {
@ -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 renameCandidatesCts = new CancellationTokenSource(cts2.token);
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();
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;
}
const requestRenameSuggestions = (cts: CancellationToken) => newSymbolNamesProviders.map(p => p.provideNewSymbolNames(model, loc.range, cts));
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 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');
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 { IDimension } from 'vs/editor/common/core/dimension';
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 { NewSymbolName, NewSymbolNameTag, ProviderResult } from 'vs/editor/common/languages';
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}
*/
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;
cancelInput(focusEditor: boolean, caller: string): void;
@ -94,7 +100,7 @@ interface IRenameInputField {
focusPreviousRenameSuggestion(): void;
}
export class RenameInputField implements IRenameInputField, IContentWidget, IDisposable {
export class RenameWidget implements IRenameInputField, IContentWidget, IDisposable {
// implement IContentWidget
readonly allowEditorOverflow: boolean = true;
@ -127,6 +133,8 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
*/
private _timeBeforeFirstInputFieldEdit: number | undefined;
private _renameCandidateProvidersCts: CancellationTokenSource | undefined;
private readonly _visibleContextKey: IContextKey<boolean>;
private readonly _disposables = new DisposableStore();
@ -194,6 +202,9 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
this._isEditingRenameCandidate = true;
}
this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed();
if (this._renameCandidateProvidersCts?.token.isCancellationRequested === false) {
this._renameCandidateProvidersCts.cancel();
}
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;
@ -365,8 +388,12 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
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
this._updateRenameCandidates(candidates, currentName, cts.token);
disposeOnDone.add(toDisposable(() => {
if (this._renameCandidateProvidersCts !== undefined) {
this._renameCandidateProvidersCts.dispose(true);
this._renameCandidateProvidersCts = undefined;
}
}));
const inputResult = new DeferredPromise<RenameInputFieldResult | boolean>();
@ -433,6 +460,24 @@ export class RenameInputField implements IRenameInputField, IContentWidget, IDis
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 {
this._trace('invoking _show');
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);
}
private _trace(...args: any[]) {
this._logService.trace('RenameInputField', ...args);
private _trace(...args: unknown[]) {
this._logService.trace('RenameWidget', ...args);
}
}

View file

@ -55,7 +55,7 @@ export interface IActionDescriptor {
*/
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;
/**

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

@ -1251,7 +1251,7 @@ declare namespace monaco.editor {
*/
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;
/**

View file

@ -59,7 +59,7 @@
.quick-input-header {
display: flex;
padding: 8px 6px 6px 6px;
padding: 8px 6px 2px 6px;
}
.quick-input-widget.hidden-input .quick-input-header {
@ -323,12 +323,21 @@
background: none;
}
/* Quick input separators as full-row item */
.quick-input-list .quick-input-list-separator-as-item {
font-weight: 600;
padding: 4px 6px;
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 */
.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;

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
// and adjust the filtering to exclude the prefix from filtering
const disposables = new DisposableStore();
@ -148,6 +152,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
// on the onDidHide event.
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
if (pick) {
return pickPromise?.p;

View file

@ -723,7 +723,15 @@ export class QuickPick<T extends IQuickPickItem> extends QuickInput implements I
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.valueSelectionUpdated = true;
this.update();
@ -1167,7 +1175,15 @@ export class InputBox extends QuickInput implements IInputBox {
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.valueSelectionUpdated = true;
this.update();

View file

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

View file

@ -277,9 +277,8 @@ class QuickPickSeparatorElement extends BaseQuickPickItemElement {
class QuickInputItemDelegate implements IListVirtualDelegate<IQuickPickElement> {
getHeight(element: IQuickPickElement): number {
if (!element.item) {
// must be a separator
return 24;
if (element instanceof QuickPickSeparatorElement) {
return 30;
}
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 { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
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 { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
type AgentData = {
interface AgentData {
dispose: () => void;
name: string;
id: string;
extensionId: ExtensionIdentifier;
hasFollowups?: boolean;
};
}
@extHostNamedCustomer(MainContext.MainThreadChatAgents2)
export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 {
@ -48,7 +48,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IChatContributionService private readonly _chatContributionService: IChatContributionService,
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2);
@ -59,7 +58,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._register(this._chatService.onDidPerformUserAction(e => {
if (typeof e.agentId === 'string') {
for (const [handle, agent] of this._agents) {
if (agent.name === e.agentId) {
if (agent.id === e.agentId) {
if (e.action.kind === 'vote') {
this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction);
} else {
@ -76,10 +75,16 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._agents.deleteAndDispose(handle);
}
$registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void {
const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name);
if (!staticAgentRegistration && !allowDynamic) {
throw new Error(`chatParticipant must be declared in package.json: ${name}`);
$registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string } | undefined): void {
const staticAgentRegistration = this._chatAgentService.getAgent(id);
if (!staticAgentRegistration && !dynamicProps) {
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 = {
@ -107,10 +112,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
};
let disposable: IDisposable;
if (!staticAgentRegistration && allowDynamic) {
if (!staticAgentRegistration && dynamicProps) {
disposable = this._chatAgentService.registerDynamicAgent(
{
id: name,
id,
name: dynamicProps.name,
description: dynamicProps.description,
extensionId: extension,
metadata: revive(metadata),
slashCommands: [],
@ -118,11 +125,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
},
impl);
} else {
disposable = this._chatAgentService.registerAgent(name, impl);
disposable = this._chatAgentService.registerAgentImplementation(id, impl);
}
this._agents.set(handle, {
name,
id: id,
extensionId: extension,
dispose: disposable.dispose,
hasFollowups: metadata.hasFollowups
});
@ -134,7 +142,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
throw new Error(`No agent with handle ${handle} registered`);
}
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> {
@ -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 agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart);
const thisAgentName = this._agents.get(handle)?.name;
if (agentPart?.agent.id !== thisAgentName) {
const thisAgentId = this._agents.get(handle)?.id;
if (agentPart?.agent.id !== thisAgentId) {
return;
}

View file

@ -1427,10 +1427,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'mappedEditsProvider');
return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider);
},
createChatParticipant(name: string, handler: vscode.ChatExtendedRequestHandler) {
createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) {
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

View file

@ -1209,7 +1209,7 @@ export interface IExtensionChatAgentMetadata extends Dto<IChatAgentMetadata> {
}
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;
$unregisterAgentCompletionsProvider(handle: number): void;
$updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void;

View file

@ -122,7 +122,7 @@ class ChatAgentResponseStream {
},
push(part) {
throwIfDone(this.push);
const dto = typeConvert.ChatResponsePart.to(part);
const dto = typeConvert.ChatResponsePart.to(part, that._commandsConverter, that._sessionDisposables);
_report(dto);
return this;
},
@ -166,12 +166,21 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
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 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._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;
}
@ -231,11 +240,11 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
{ ...ehResult, metadata: undefined };
// 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
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;
@ -338,7 +347,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
class ExtHostChatAgent {
private _followupProvider: vscode.ChatFollowupProvider | undefined;
private _description: string | undefined;
private _fullName: string | undefined;
private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined;
private _isDefault: boolean | undefined;
@ -437,7 +445,6 @@ class ExtHostChatAgent {
updateScheduled = true;
queueMicrotask(() => {
this._proxy.$updateAgent(this._handle, {
description: this._description,
fullName: this._fullName,
icon: !this._iconPath ? undefined :
this._iconPath instanceof URI ? this._iconPath :
@ -463,16 +470,9 @@ class ExtHostChatAgent {
const that = this;
return {
get name() {
get id() {
return that.id;
},
get description() {
return that._description ?? '';
},
set description(v) {
that._description = v;
updateMetadataSoon();
},
get fullName() {
checkProposedApiEnabled(that.extension, 'defaultChatParticipant');
return that._fullName ?? that.extension.displayName ?? that.extension.name;

View file

@ -109,8 +109,13 @@ class ChatVariableResolverResponseStream {
},
push(part) {
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;
}
};

View file

@ -2454,7 +2454,7 @@ export namespace ChatResponseReferencePart {
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) {
return ChatResponseMarkdownPart.to(part);
} else if (part instanceof types.ChatResponseAnchorPart) {
@ -2465,6 +2465,8 @@ export namespace ChatResponsePart {
return ChatResponseProgressPart.to(part);
} else if (part instanceof types.ChatResponseFileTreePart) {
return ChatResponseFilesPart.to(part);
} else if (part instanceof types.ChatResponseCommandButtonPart) {
return ChatResponseCommandButtonPart.to(part, commandsConverter, commandDisposables);
}
return {
kind: 'content',
@ -2546,7 +2548,7 @@ export namespace ChatResponseProgress {
};
} else if ('participant' in progress) {
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) {
return { content: MarkdownString.from(progress.message), kind: 'progressMessage' };
} else {

View file

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

View file

@ -148,7 +148,7 @@ class EntitlementsContribution extends Disposable implements IWorkbenchContribut
}
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 showWelcomeView = this.configurationService.inspect<boolean>(chatWelcomeViewConfigKey).value ?? false;

View file

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

View file

@ -251,7 +251,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
executeImmediately: true
}, async (prompt, progress) => {
const defaultAgent = chatAgentService.getDefaultAgent();
const agents = chatAgentService.getRegisteredAgents();
const agents = chatAgentService.getAgents();
// Report prefix
if (defaultAgent?.metadata.helpTextPrefix) {
@ -270,7 +270,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
const agentWithLeader = `${chatAgentLeader}${a.id}`;
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` };
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 actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));

View file

@ -112,6 +112,7 @@ export interface IChatWidget {
readonly providerId: string;
readonly supportsFileReferences: boolean;
readonly parsedInput: IParsedChatRequest;
lastSelectedAgent: IChatAgentData | undefined;
getContrib<T extends IChatWidgetContrib>(id: string): T | undefined;
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.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
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 { registerAction2 } from 'vs/platform/actions/common/actions';
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 { ILogService } from 'vs/platform/log/common/log';
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 { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
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 * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry';
@ -64,20 +67,24 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi
const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({
extensionPoint: 'chatParticipants',
jsonSchema: {
description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'),
description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a chat participant'),
type: 'array',
items: {
additionalProperties: false,
type: 'object',
defaultSnippets: [{ body: { name: '', description: '' } }],
required: ['name'],
required: ['name', 'id'],
properties: {
id: {
description: localize('chatParticipantId', "A unique id for this chat participant."),
type: 'string'
},
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'
},
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'
},
isDefault: {
@ -92,7 +99,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
}
},
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',
items: {
additionalProperties: false,
@ -131,7 +138,7 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi
}
},
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',
default: ['panel'],
items: {
@ -158,12 +165,14 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
private _welcomeViewDescriptor?: IViewDescriptor;
private _viewContainer: ViewContainer;
private _registrationDisposables = new Map<string, IDisposable>();
private _participantRegistrationDisposables = new DisposableMap<string>();
constructor(
@IChatContributionService readonly _chatContributionService: IChatContributionService,
@IProductService readonly productService: IProductService,
@IContextKeyService readonly contextService: IContextKeyService,
@ILogService readonly logService: ILogService,
@IChatContributionService private readonly _chatContributionService: IChatContributionService,
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
@IProductService private readonly productService: IProductService,
@IContextKeyService private readonly contextService: IContextKeyService,
@ILogService private readonly logService: ILogService,
) {
this._viewContainer = this.registerViewContainer();
this.registerListeners();
@ -243,13 +252,34 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution {
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 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);
function getParticipantKey(participant: IChatParticipantContribution): string {
return `${participant.extensionId.value}_${participant.name}`;
function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string {
return `${extensionId.value}_${participantName}`;
}
export class ChatContributionService implements IChatContributionService {
declare _serviceBrand: undefined;
private _registeredProviders = new Map<string, IChatProviderContribution>();
private _registeredParticipants = new Map<string, IChatParticipantContribution>();
constructor(
) { }
@ -339,19 +368,7 @@ export class ChatContributionService implements IChatContributionService {
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[] {
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 {
let progressMsg: string = '';
if (element.agent && !element.agent.isDefault) {
let usingMsg = chatAgentLeader + element.agent.id;
let usingMsg = chatAgentLeader + element.agent.name;
if (element.slashCommand) {
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 { ILabelService } from 'vs/platform/label/common/label';
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';
const variableRefUrl = 'http://_vscodedecoration_';
@ -31,7 +31,9 @@ export class ChatMarkdownDecorationsRenderer {
} else {
const uri = part instanceof ChatRequestDynamicVariablePart && part.data.map(d => d.value).find((d): d is URI => d instanceof URI)
|| 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})`;
}
@ -106,4 +108,3 @@ export class ChatMarkdownDecorationsRenderer {
}
}
}

View file

@ -145,7 +145,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
private parsedChatRequest: IParsedChatRequest | undefined;
get parsedInput() {
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;
@ -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 {
return !!this.viewOptions.supportsFileReferences;
}
@ -655,7 +664,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
'query' in opts ? opts.query :
`${opts.prefix} ${editorValue}`;
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) {
const inputState = this.collectInputState();

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
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 { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
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 { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@ -37,8 +39,8 @@ const placeholderDecorationType = 'chat-session-detail';
const slashCommandTextDecorationType = 'chat-session-text';
const variableTextDecorationType = 'chat-variable-text';
function agentAndCommandToKey(agent: string, subcommand: string | undefined): string {
return subcommand ? `${agent}__${subcommand}` : agent;
function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string {
return subcommand ? `${agent.id}__${subcommand}` : agent.id;
}
class InputEditorDecorations extends Disposable {
@ -70,7 +72,7 @@ class InputEditorDecorations extends Disposable {
this.updateInputEditorDecorations();
}));
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()));
@ -135,7 +137,7 @@ class InputEditorDecorations extends Disposable {
},
renderOptions: {
after: {
contentText: viewModel.inputPlaceholder ?? defaultAgent?.metadata.description ?? '',
contentText: viewModel.inputPlaceholder ?? defaultAgent?.description ?? '',
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);
if (onlyAgentAndWhitespace) {
// 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;
if (agentPart.agent.metadata.description && exactlyOneSpaceAfterPart(agentPart)) {
if (agentPart.agent.description && exactlyOneSpaceAfterPart(agentPart)) {
placeholderDecoration = [{
range: getRangeForPlaceholder(agentPart),
renderOptions: {
after: {
contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.metadata.description,
contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.description,
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);
if (onlyAgentCommandAndWhitespace) {
// 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;
if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) {
placeholderDecoration = [{
@ -209,9 +211,10 @@ class InputEditorDecorations extends Disposable {
const textDecorations: IDecorationOptions[] | undefined = [];
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) {
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) {
let value: string | undefined;
if (slashCommand && slashCommand.isSticky) {
value = `${chatAgentLeader}${agent.id} ${chatSubcommandLeader}${slashCommand.name} `;
value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `;
} else if (agent.metadata.isSticky) {
value = `${chatAgentLeader}${agent.id} `;
value = `${chatAgentLeader}${agent.name} `;
}
if (value) {
@ -347,13 +350,18 @@ class AgentCompletions extends Disposable {
const agents = this.chatAgentService.getAgents()
.filter(a => !a.isDefault);
return <CompletionList>{
suggestions: agents.map((c, i) => {
const withAt = `@${c.id}`;
suggestions: agents.map((a, i) => {
const withAt = `@${a.name}`;
const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id);
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} `,
detail: c.metadata.description,
detail: a.description,
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
};
})
@ -431,31 +439,37 @@ class AgentCompletions extends Disposable {
const justAgents: CompletionItem[] = agents
.filter(a => !a.isDefault)
.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 {
label: { label: agentLabel, description: agent.metadata.description },
filterText: `${chatSubcommandLeader}${agent.id}`,
label: isDupe ?
{ label: agentLabel, description: agent.description, detail: ` (${agent.id})` } :
agentLabel,
detail,
filterText: `${chatSubcommandLeader}${agent.name}`,
insertText: `${agentLabel} `,
range: new Range(1, 1, 1, 1),
kind: CompletionItemKind.Text,
sortText: `${chatSubcommandLeader}${agent.id}`,
sortText: `${chatSubcommandLeader}${agent.name}`,
};
});
return {
suggestions: justAgents.concat(
agents.flatMap(agent => agent.slashCommands.map((c, i) => {
const agentLabel = `${chatAgentLeader}${agent.id}`;
const agentLabel = `${chatAgentLeader}${agent.name}`;
const withSlash = `${chatSubcommandLeader}${c.name}`;
return {
label: { label: withSlash, description: agentLabel },
filterText: `${chatSubcommandLeader}${agent.id}${c.name}`,
filterText: `${chatSubcommandLeader}${agent.name}${c.name}`,
commitCharacters: [' '],
insertText: `${agentLabel} ${withSlash} `,
detail: `(${agentLabel}) ${c.description ?? ''}`,
range: new Range(1, 1, 1, 1),
kind: CompletionItemKind.Text, // The icons are disabled here anyway
sortText: `${chatSubcommandLeader}${agent.id}${c.name}`,
sortText: `${chatSubcommandLeader}${agent.name}${c.name}`,
} satisfies CompletionItem;
})))
};
@ -465,6 +479,32 @@ class AgentCompletions extends Disposable {
}
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 {
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 inputValue = this.widget.inputEditor.getValue();
let previousInputValue: string | undefined;
let previousSelectedAgent: IChatAgentData | undefined;
// 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.
this._register(this.widget.inputEditor.onDidChangeModelContent(e => {
if (!previousInputValue) {
previousInputValue = inputValue;
previousSelectedAgent = this.widget.lastSelectedAgent;
}
// 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 (!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
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();
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.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray, distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
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 { URI } from 'vs/base/common/uri';
import { ProviderResult } from 'vs/editor/common/languages';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
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 { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
@ -47,6 +46,8 @@ export namespace ChatAgentLocation {
export interface IChatAgentData {
id: string;
name: string;
description?: string;
extensionId: ExtensionIdentifier;
/** The agent invoked when no agent is specified */
isDefault?: boolean;
@ -79,7 +80,6 @@ export interface IChatRequesterInformation {
}
export interface IChatAgentMetadata {
description?: string;
helpTextPrefix?: string | IMarkdownString;
helpTextVariablesPrefix?: string | IMarkdownString;
helpTextPostfix?: string | IMarkdownString;
@ -118,86 +118,102 @@ export interface IChatAgentResult {
export const IChatAgentService = createDecorator<IChatAgentService>('chatAgentService');
interface IChatAgentEntry {
data: IChatAgentData;
impl?: IChatAgentImplementation;
}
export interface IChatAgentService {
_serviceBrand: undefined;
/**
* undefined when an agent was removed IChatAgent
*/
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;
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[]>;
getAgents(): IChatAgentData[];
getRegisteredAgents(): Array<IChatAgentData>;
getActivatedAgents(): Array<IChatAgent>;
getAgent(id: string): IChatAgentData | undefined;
getAgents(): IChatAgentData[];
getActivatedAgents(): Array<IChatAgent>;
getAgentsByName(name: string): IChatAgentData[];
getDefaultAgent(): IChatAgent | undefined;
getSecondaryAgent(): IChatAgentData | undefined;
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void;
}
export class ChatAgentService extends Disposable implements IChatAgentService {
export class ChatAgentService implements IChatAgentService {
public static readonly AGENT_LEADER = '@';
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;
constructor(
@IChatContributionService private chatContributionService: IChatContributionService,
@IContextKeyService private contextKeyService: IContextKeyService,
) {
super();
}
@IContextKeyService private readonly contextKeyService: IContextKeyService
) { }
override dispose(): void {
super.dispose();
this._agents.clear();
}
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}`);
registerAgent(id: string, data: IChatAgentData): IDisposable {
const existingAgent = this.getAgent(id);
if (existingAgent) {
throw new Error(`Agent already registered: ${JSON.stringify(id)}`);
}
const data = this.getAgent(name);
if (!data) {
throw new Error(`Unknown agent: ${name}`);
const that = this;
const commands = data.slashCommands;
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 };
this._agents.set(name, agent);
this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl));
if (entry.impl) {
throw new Error(`Agent already has implementation: ${JSON.stringify(id)}`);
}
entry.impl = agentImpl;
this._onDidChangeAgents.fire(new MergedChatAgent(entry.data, agentImpl));
return toDisposable(() => {
if (this._agents.delete(name)) {
this._onDidChangeAgents.fire(undefined);
}
entry.impl = undefined;
this._onDidChangeAgents.fire(undefined);
});
}
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable {
const agent = { data, impl: agentImpl };
this._agents.set(data.id, agent);
this._agents.push(agent);
this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl));
return toDisposable(() => {
if (this._agents.delete(data.id)) {
this._onDidChangeAgents.fire(undefined);
}
this._agents = this._agents.filter(a => a !== agent);
this._onDidChangeAgents.fire(undefined);
});
}
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void {
const agent = this._agents.get(id);
const agent = this._getAgentEntry(id);
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 };
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;
}
getRegisteredAgents(): Array<IChatAgentData> {
const that = this;
return this.chatContributionService.registeredParticipants.map(p => (
{
extensionId: p.extensionId,
id: p.name,
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));
private _getAgentEntry(id: string): IChatAgentEntry | undefined {
return this._agents.find(a => a.data.id === id);
}
getAgent(id: string): IChatAgentData | undefined {
return this._getAgentEntry(id)?.data;
}
/**
* Returns all agent datas that exist- static registered and dynamic ones.
*/
getAgents(): IChatAgentData[] {
const registeredAgents = this.getRegisteredAgents();
const dynamicAgents = Array.from(this._agents.values()).map(a => a.data);
const all = [
...registeredAgents,
...dynamicAgents
];
return distinct(all, a => a.id);
return this._agents.map(entry => entry.data);
}
getActivatedAgents(): IChatAgent[] {
@ -249,12 +249,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
.map(a => new MergedChatAgent(a.data, a.impl!));
}
getAgent(id: string): IChatAgentData | undefined {
return this.getAgents().find(a => a.id === id);
getAgentsByName(name: string): IChatAgentData[] {
return this.getAgents().filter(a => a.name === name);
}
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) {
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[]> {
const data = this._agents.get(id);
const data = this._getAgentEntry(id);
if (!data?.impl) {
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 name(): string { return this.data.name ?? ''; }
get description(): string { return this.data.description ?? ''; }
get extensionId(): ExtensionIdentifier { return this.data.extensionId; }
get isDefault(): boolean | undefined { return this.data.isDefault; }
get metadata(): IChatAgentMetadata { return this.data.metadata; }

View file

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

View file

@ -591,7 +591,7 @@ export class ChatModel extends Disposable implements IChatModel {
const request = new ChatRequestModel(this, parsedRequest, variableData);
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
revive<ISerializableChatAgentData>(raw.agent) : undefined;
this.reviveSerializedAgent(raw.agent) : undefined;
// Port entries from old format
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 {
// 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)];
@ -703,7 +713,7 @@ export class ChatModel extends Disposable implements IChatModel {
} else if (progress.kind === 'usedContext' || progress.kind === 'reference') {
request.response.applyReference(progress);
} else if (progress.kind === 'agentDetection') {
const agent = this.chatAgentService.getAgent(progress.agentName);
const agent = this.chatAgentService.getAgent(progress.agentId);
if (agent) {
request.response.setAgent(agent, progress.command);
}
@ -802,7 +812,7 @@ export class ChatModel extends Disposable implements IChatModel {
vote: r.response?.vote,
agent: r.response?.agent ?
// 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,
slashCommand: r.response?.slashCommand,
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) { }
get text(): string {
return `${chatAgentLeader}${this.agent.id}`;
return `${chatAgentLeader}${this.agent.name}`;
}
get promptText(): string {
@ -92,6 +92,8 @@ export class ChatRequestAgentPart implements IParsedChatRequestPart {
editorRange: this.editorRange,
agent: {
id: this.agent.id,
name: this.agent.name,
description: this.agent.description,
metadata: this.agent.metadata
}
};
@ -167,10 +169,19 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed
(part as ChatRequestVariablePart).variableArg
);
} 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(
new OffsetRange(part.range.start, part.range.endExclusive),
part.editorRange,
(part as ChatRequestAgentPart).agent
agent
);
} else if (part.kind === ChatRequestAgentSubcommandPart.Kind) {
return new ChatRequestAgentSubcommandPart(

View file

@ -6,10 +6,8 @@
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
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 { 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 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 {
constructor(
@IChatAgentService private readonly agentService: IChatAgentService,
@IChatVariablesService private readonly variableService: IChatVariablesService,
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,
@IChatService private readonly chatService: IChatService
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService
) { }
parseChatRequest(sessionId: string, message: string): IParsedChatRequest {
parseChatRequest(sessionId: string, message: string, context?: IChatParserContext): IParsedChatRequest {
const parts: IParsedChatRequestPart[] = [];
const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls
const model = this.chatService.getSession(sessionId)!;
let lineNumber = 1;
let column = 1;
@ -40,9 +41,9 @@ export class ChatRequestParser {
if (char === chatVariableLeader) {
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts);
} 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) {
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) {
@ -89,17 +90,23 @@ export class ChatRequestParser {
};
}
private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
const nextVariableMatch = message.match(agentReg);
if (!nextVariableMatch) {
private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined {
const nextAgentMatch = message.match(agentReg);
if (!nextAgentMatch) {
return;
}
const [full, name] = nextVariableMatch;
const varRange = new OffsetRange(offset, offset + full.length);
const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
const [full, name] = nextAgentMatch;
const agentRange = new OffsetRange(offset, offset + 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) {
return;
}
@ -121,7 +128,7 @@ export class ChatRequestParser {
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 {
@ -142,7 +149,7 @@ export class ChatRequestParser {
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);
if (!nextSlashMatch) {
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 { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
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';
export interface IChat {
@ -85,7 +86,7 @@ export interface IChatContentInlineReference {
}
export interface IChatAgentDetection {
agentName: string;
agentId: string;
command?: IChatAgentCommand;
kind: 'agentDetection';
}
@ -283,7 +284,7 @@ export interface IChatService {
/**
* 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>;
cancelCurrentRequestForSession(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 { 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 { 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 { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
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);
}
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 ? '[...]' : ''}}`);
if (!request.trim()) {
this.trace('sendRequest', 'Rejected empty message');
@ -458,7 +458,7 @@ export class ChatService extends Disposable implements IChatService {
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 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(IChatVariablesService, service);
instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
});
test('ChatVariables - resolveVariables', async function () {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,12 @@
},
agent: {
id: "agent",
name: "agent",
extensionId: {
value: "nullExtensionDescription",
_lower: "nullextensiondescription"
},
locations: [ ],
metadata: { description: "" },
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(ILogService, new NullLogService());
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
});
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 { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
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';
suite('ChatRequestParser', () => {
@ -31,7 +31,7 @@ suite('ChatRequestParser', () => {
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IExtensionService, new TestExtensionService());
instantiationService.stub(IChatService, new MockChatService());
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
instantiationService.stub(IChatAgentService, instantiationService.createInstance(ChatAgentService));
varService = mockObject<IChatVariablesService>()({});
varService.getDynamicVariables.returns([]);
@ -112,12 +112,12 @@ suite('ChatRequestParser', () => {
});
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 () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@ -127,7 +127,7 @@ suite('ChatRequestParser', () => {
test('agents, subCommand', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@ -137,7 +137,7 @@ suite('ChatRequestParser', () => {
test('agent with question mark', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@ -147,7 +147,7 @@ suite('ChatRequestParser', () => {
test('agent and subcommand with leading whitespace', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@ -157,7 +157,7 @@ suite('ChatRequestParser', () => {
test('agent and subcommand after newline', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@ -167,7 +167,7 @@ suite('ChatRequestParser', () => {
test('agent not first', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
parser = instantiationService.createInstance(ChatRequestParser);
@ -177,7 +177,7 @@ suite('ChatRequestParser', () => {
test('agents and variables and multiline', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
varService.hasVariable.returns(true);
@ -189,7 +189,7 @@ suite('ChatRequestParser', () => {
test('agents and variables and multiline, part2', async () => {
const agentsService = mockObject<IChatAgentService>()({});
agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }]));
agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]);
instantiationService.stub(IChatAgentService, agentsService as any);
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 { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
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 { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
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 { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
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 { 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 { 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 {
private static sessionId = 0;
@ -57,6 +56,7 @@ class SimpleTestProvider extends Disposable implements IChatProvider {
const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext';
const chatAgentWithUsedContext: IChatAgent = {
id: chatAgentWithUsedContextId,
name: chatAgentWithUsedContextId,
extensionId: nullExtensionDescription.identifier,
locations: [ChatAgentLocation.Panel],
metadata: {},
@ -82,7 +82,7 @@ const chatAgentWithUsedContext: IChatAgent = {
},
};
suite('Chat', () => {
suite('ChatService', () => {
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
let storageService: IStorageService;
@ -104,13 +104,8 @@ suite('Chat', () => {
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)));
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);
const agent = {
@ -118,7 +113,9 @@ suite('Chat', () => {
return {};
},
} 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' });
});
@ -209,7 +206,7 @@ suite('Chat', () => {
});
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' });
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider'))));
@ -230,7 +227,7 @@ suite('Chat', () => {
test('can deserialize', async () => {
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
{ // serapate block to not leak variables in outer scope

View file

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

View file

@ -28,7 +28,10 @@ suite('VoiceChat', () => {
extensionId: ExtensionIdentifier = nullExtensionDescription.identifier;
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.'); }
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); }
metadata = {};
@ -47,17 +50,18 @@ suite('VoiceChat', () => {
class TestChatAgentService implements IChatAgentService {
_serviceBrand: undefined;
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.'); }
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(); }
getRegisteredAgents(): Array<IChatAgent> { return agents; }
getActivatedAgents(): IChatAgent[] { return agents; }
getAgents(): IChatAgent[] { return agents; }
getAgent(id: string): IChatAgent | undefined { throw new Error(); }
getDefaultAgent(): 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 {

View file

@ -7,16 +7,20 @@ import * as assert from 'assert';
import { equals } from 'vs/base/common/arrays';
import { timeout } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { mock } from 'vs/base/test/common/mock';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
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 { 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 { ITextModel } from 'vs/editor/common/model';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
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 { IConfigurationService } from 'vs/platform/configuration/common/configuration';
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 { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
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 { 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 { 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 { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl';
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 { 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 () {
class TestController extends InlineChatController {
@ -117,8 +114,6 @@ suite('InteractiveChatController', function () {
const serviceCollection = new ServiceCollection(
[IEditorWorkerService, new SyncDescriptor(TestWorkerService)],
[IContextKeyService, contextKeyService],
[IChatContributionService, new MockChatContributionService(
[{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])],
[IChatAgentService, new SyncDescriptor(ChatAgentService)],
[IInlineChatService, inlineChatService],
[IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)],
@ -152,15 +147,7 @@ suite('InteractiveChatController', function () {
}]
);
instaService = 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));
instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection));
inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService));
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 { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
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 { 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 { CellEditType, CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
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
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 TOGGLE_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.toggleOutputs';
const TOGGLE_CELL_OUTPUT_SCROLLING = 'notebook.cell.toggleOutputScrolling';
const OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID = 'notebook.cell.openFailureActions';
registerAction2(class CollapseCellInputAction extends NotebookMultiCellAction {
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
function forEachCell(editor: INotebookEditor, callback: (cell: ICellViewModel, index: number) => void) {

View file

@ -70,7 +70,9 @@ export class CellDiagnostics extends Disposable {
}
public clear() {
this.clearDiagnostics();
if (this.ErrorDetails) {
this.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 { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
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';
export class CellContextKeyPart extends CellContentPart {
@ -47,6 +47,7 @@ export class CellContextKeyManager extends Disposable {
private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>;
private cellResource!: IContextKey<string>;
private cellGeneratedByChat!: IContextKey<boolean>;
private cellHasErrorDiagnostics!: 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.cellGeneratedByChat = NOTEBOOK_CELL_GENERATED_BY_CHAT.bindTo(this._contextKeyService);
this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService);
this.cellHasErrorDiagnostics = NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS.bindTo(this._contextKeyService);
if (element) {
this.updateForElement(element);
@ -200,6 +202,10 @@ export class CellContextKeyManager extends Disposable {
this.cellRunState.set('idle');
this.cellExecuting.set(false);
}
if (this.element instanceof CodeCellViewModel) {
this.cellHasErrorDiagnostics.set(!!this.element.cellErrorDetails);
}
}
private updateForEditState() {

View file

@ -47,6 +47,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
private _outputCollection: number[] = [];
private readonly _cellDiagnostics: CellDiagnostics;
get cellErrorDetails() {
return this._cellDiagnostics.ErrorDetails;
}
private _outputsTop: PrefixSumComputer | null = null;
@ -171,7 +174,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
if (outputLayoutChange) {
this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs');
}
if (this._outputCollection.length === 0 && this._cellDiagnostics.ErrorDetails) {
if (this._outputCollection.length === 0) {
this._cellDiagnostics.clear();
}
dispose(removedOutputs);
@ -433,6 +436,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
protected onDidChangeTextModelContent(): void {
if (this.getEditState() !== CellEditState.Editing) {
this._cellDiagnostics.clear();
this.updateEditState(CellEditState.Editing, 'onDidChangeTextModelContent');
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_RESOURCE = new RawContextKey<string>('notebookCellResource', '');
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
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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatAgentRequest, IChatAgentService, ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents';
import { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
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';
@ -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 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());
@ -97,7 +98,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr
@IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
@IChatService private readonly _chatService: IChatService,
@IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService
@IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService,
) {
super();
@ -113,14 +114,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr
return;
}
if (!this._chatAgentService.getAgent(this._terminalAgentId)) {
this._register(this._chatAgentService.onDidChangeAgents(() => {
if (this._chatAgentService.getAgent(this._terminalAgentId)) {
this._terminalAgentRegisteredContextKey.set(true);
}
}));
} else {
this._terminalAgentRegisteredContextKey.set(true);
if (!this.initTerminalAgent()) {
this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent()));
}
this._register(this._chatCodeBlockContextProviderService.registerProvider({
getCodeBlockContext: (editor) => {
@ -141,6 +136,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr
}, '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 {
if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) {
return;
@ -285,13 +291,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr
const requestProps: IChatAgentRequest = {
sessionId: model.sessionId,
requestId: this._currentRequest!.id,
agentId: this._terminalAgentId,
agentId: this._terminalAgentId!,
message: this._lastInput,
variables: { variables: [] },
location: ChatAgentLocation.Terminal
};
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.updateFollowUps(undefined);
this._chatWidget?.value.inlineChatWidget.updateProgress(true);

View file

@ -21,9 +21,9 @@ declare module 'vscode' {
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.
@ -35,7 +35,7 @@ declare module 'vscode' {
*/
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;
/**
* 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.
*/
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 {
@ -158,7 +158,7 @@ declare module 'vscode' {
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.
*/
participant?: string;
@ -192,9 +192,9 @@ declare module 'vscode' {
*/
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.
@ -446,12 +446,11 @@ declare module 'vscode' {
/**
* 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
* contributing the participant but can collide with names from other extensions.
* @param id A unique identifier for the participant.
* @param handler A request handler for the 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
*/
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[]>;
}
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;
}
}