Merge branch 'main' into mrleemurray/inadequate-hamster-teal

This commit is contained in:
Lee Murray
2025-07-03 11:38:51 +01:00
committed by GitHub
30 changed files with 230 additions and 108 deletions
@@ -1,9 +1,6 @@
pr: none
trigger:
batch: true
branches:
include: ["main"]
trigger: none
parameters:
- name: VSCODE_QUALITY
@@ -824,6 +824,18 @@ export const codeTunnelSubcommands: Fig.Subcommand[] = [
template: 'filepaths',
},
},
{
name: ['--maximize'],
description: 'Maximize the chat session view.',
},
{
name: ['-r', '--reuse-window'],
description: 'Force to use the last active window for the chat session',
},
{
name: ['-n', '--new-window'],
description: 'Force to open an empty window for the chat session',
},
{
name: ['-h', '--help'],
description: 'Print usage',
@@ -72,7 +72,7 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] {
const categoryOptions = ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'];
const logOptions = ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'];
const syncOptions = ['on', 'off'];
const chatOptions = ['--add-file <file>', '--help', '--mode <mode>', '-a <file>', '-h', '-m <mode>'];
const chatOptions = ['--add-file <file>', '--help', '--maximize', '--mode <mode>', '--new-window', '--reuse-window', '-a <file>', '-h', '-m <mode>', '-n', '-r'];
const typingTests: ITestSpec[] = [];
for (let i = 1; i < executable.length; i++) {
@@ -281,7 +281,7 @@ export function createCodeTunnelTestSpecs(executable: string): ITestSpec[] {
{ input: `${executable} tunnel unregister |`, expectedCompletions: [...commonFlags] },
{ input: `${executable} tunnel service |`, expectedCompletions: [...commonFlags, 'help', 'install', 'log', 'uninstall'] },
{ input: `${executable} tunnel help |`, expectedCompletions: helpSubcommands },
{ input: `${executable} chat |`, expectedCompletions: ['--mode <mode>', '--add-file <file>', '--help', '-m <mode>', '-a <file>', '-h'] },
{ input: `${executable} chat |`, expectedCompletions: ['--mode <mode>', '--add-file <file>', '--help', '--maximize', '--new-window', '--reuse-window', '-m <mode>', '-a <file>', '-h', '-n', '-r'] },
{ input: `${executable} chat --mode |`, expectedCompletions: ['agent', 'ask', 'edit'] },
{ input: `${executable} chat --add-file |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } },
{ input: `${executable} serve-web |`, expectedCompletions: serveWebSubcommandsAndFlags },
+1 -1
View File
@@ -727,7 +727,7 @@ function completeSingleLinePattern(token: marked.Tokens.Text | marked.Tokens.Par
}
// Contains the start of link text, and no following tokens contain the link target
else if (lastLine.match(/(^|\s)\[\w*/)) {
else if (lastLine.match(/(^|\s)\[\w*[^\]]*$/)) {
return completeLinkText(token);
}
}
@@ -958,6 +958,14 @@ suite('MarkdownRenderer', () => {
assert.deepStrictEqual(newTokens, tokens);
});
test('square braces in text', () => {
const incomplete = 'hello [what] is going on';
const tokens = marked.marked.lexer(incomplete);
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, tokens);
});
test('complete link', () => {
const incomplete = 'text [link](http://microsoft.com)';
const tokens = marked.marked.lexer(incomplete);
+12 -3
View File
@@ -507,9 +507,18 @@ class CodeMain {
}
if (args.chat) {
// If we are started with chat subcommand, the current working
// directory is always the path to open
args._ = [cwd()];
if (args.chat['new-window']) {
// Apply `--new-window` flag to the main arguments
args['new-window'] = true;
} else if (args.chat['reuse-window']) {
// Apply `--reuse-window` flag to the main arguments
args['reuse-window'] = true;
} else {
// Unless we are started with specific instructions about
// new windows or reusing existing ones, always take the
// current working directory as workspace to open.
args._ = [cwd()];
}
}
return args;
@@ -28,6 +28,9 @@ export interface NativeParsedArgs {
_: string[];
'add-file'?: string[];
mode?: string;
maximize?: boolean;
'reuse-window'?: boolean;
'new-window'?: boolean;
help?: boolean;
};
+4 -1
View File
@@ -55,7 +55,10 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'_': { type: 'string[]', description: localize('prompt', "The prompt to use as chat.") },
'mode': { type: 'string', cat: 'o', alias: 'm', args: 'mode', description: localize('chatMode', "The mode to use for the chat session. Available options: 'ask', 'edit', 'agent', or the identifier of a custom mode. Defaults to 'agent'.") },
'add-file': { type: 'string[]', cat: 'o', alias: 'a', args: 'path', description: localize('addFile', "Add files as context to the chat session.") },
'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") }
'maximize': { type: 'boolean', cat: 'o', description: localize('chatMaximize', "Maximize the chat session view.") },
'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindowForChat', "Force to use the last active window for the chat session.") },
'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindowForChat', "Force to open an empty window for the chat session.") },
'help': { type: 'boolean', alias: 'h', description: localize('help', "Print usage.") }
}
},
'serve-web': {
@@ -8,7 +8,7 @@ import { DeferredPromise, RunOnceScheduler, timeout, Delayer } from '../../../ba
import { CancellationToken } from '../../../base/common/cancellation.js';
import { toErrorMessage } from '../../../base/common/errorMessage.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { Disposable, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
import { FileAccess, Schemas } from '../../../base/common/network.js';
import { getMarks, mark } from '../../../base/common/performance.js';
import { isBigSurOrNewer, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
@@ -84,6 +84,29 @@ const enum ReadyState {
READY
}
class DockBadgeManager {
static readonly INSTANCE = new DockBadgeManager();
private readonly windows = new Set<number>();
acquireBadge(window: IBaseWindow): IDisposable {
this.windows.add(window.id);
electron.app.setBadgeCount(isLinux ? 1 /* only numbers supported */ : undefined /* generic dot */);
return {
dispose: () => {
this.windows.delete(window.id);
if (this.windows.size === 0) {
electron.app.setBadgeCount(0);
}
}
};
}
}
export abstract class BaseWindow extends Disposable implements IBaseWindow {
//#region Events
@@ -325,18 +348,18 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow {
case FocusMode.Notify:
if (isMacintosh) {
this.setFocusNotificationBadge(undefined /* generic dot */);
this.showFocusNotificationBadge();
// On macOS we have direct API to bounce the dock icon
electron.app.dock?.bounce('informational');
} else if (isWindows) {
this.setFocusNotificationBadge(undefined /* generic dot */);
this.showFocusNotificationBadge();
// On Windows, calling focus() will bounce the taskbar icon
// https://github.com/electron/electron/issues/2867
this.win?.focus();
} else if (isLinux) {
this.setFocusNotificationBadge(1 /* only number supported */);
this.showFocusNotificationBadge();
// On Linux, there seems to be no way to bounce the taskbar icon
// as calling focus() will actually steal focus away.
@@ -352,18 +375,16 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow {
}
}
private hasFocusNotificationBadge = false;
private readonly focusNotificationBadgeDisposable = this._register(new MutableDisposable());
private setFocusNotificationBadge(count?: number): void {
electron.app.setBadgeCount(count);
this.hasFocusNotificationBadge = true;
private showFocusNotificationBadge(): void {
if (!this.focusNotificationBadgeDisposable.value) {
this.focusNotificationBadgeDisposable.value = DockBadgeManager.INSTANCE.acquireBadge(this);
}
}
private clearFocusNotificationBadge(): void {
if (this.hasFocusNotificationBadge) {
electron.app.setBadgeCount(0);
this.hasFocusNotificationBadge = false;
}
this.focusNotificationBadgeDisposable.clear();
}
private doFocusWindow() {
+9 -7
View File
@@ -633,10 +633,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService, coreExperimentationService: ICoreExperimentationService): void {
this._mainContainerDimension = getClientArea(this.parent, DEFAULT_WINDOW_DIMENSIONS); // running with fallback to ensure no error is thrown (https://github.com/microsoft/vscode/issues/240242)
this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService, this.environmentService);
this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, coreExperimentationService);
this.stateModel.load({
mainContainerDimension: this._mainContainerDimension,
resetLayout: Boolean(this.layoutOptions?.resetLayout)
resetLayout: Boolean(this.layoutOptions?.resetLayout),
isAuxiliaryBarEmpty: this.viewDescriptorService
.getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar)
.find(viewContainer => this.hasViews(viewContainer.id))?.id !== undefined
});
this._register(this.stateModel.onDidChangeState(change => {
@@ -2789,6 +2792,7 @@ enum LegacyWorkbenchLayoutSettings {
interface ILayoutStateLoadConfiguration {
readonly mainContainerDimension: IDimension;
readonly resetLayout: boolean;
readonly isAuxiliaryBarEmpty: boolean;
}
class LayoutStateModel extends Disposable {
@@ -2804,8 +2808,7 @@ class LayoutStateModel extends Disposable {
private readonly storageService: IStorageService,
private readonly configurationService: IConfigurationService,
private readonly contextService: IWorkspaceContextService,
private readonly coreExperimentationService: ICoreExperimentationService,
private readonly environmentService: IBrowserWorkbenchEnvironmentService
private readonly coreExperimentationService: ICoreExperimentationService
) {
super();
@@ -2868,9 +2871,8 @@ class LayoutStateModel extends Disposable {
LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = workbenchState === WorkbenchState.EMPTY;
LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4);
LayoutStateKeys.AUXILIARYBAR_HIDDEN.defaultValue = (() => {
const configuration = this.configurationService.inspect(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY);
if (configuration.defaultValue !== 'hidden' && isWeb && !this.environmentService.remoteAuthority) {
return true; // TODO@bpasero revisit this when Chat is available in serverless web
if (configuration.isAuxiliaryBarEmpty) {
return true; // require a view in the auxiliary bar to show it by default
}
switch (this.configurationService.getValue(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY)) {
@@ -7,7 +7,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
import { localize } from '../../../../nls.js';
import { registerAction2 } from '../../../../platform/actions/common/actions.js';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
import { IExtensionManifest, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js';
@@ -20,7 +20,6 @@ import { IAuthenticationUsageService } from '../../../services/authentication/br
import { ManageAccountPreferencesForMcpServerAction } from './actions/manageAccountPreferencesForMcpServerAction.js';
import { ManageTrustedMcpServersForAccountAction } from './actions/manageTrustedMcpServersForAccountAction.js';
import { RemoveDynamicAuthenticationProvidersAction } from './actions/manageDynamicAuthenticationProvidersAction.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js';
import { IMcpRegistry } from '../../mcp/common/mcpRegistryTypes.js';
import { autorun } from '../../../../base/common/observable.js';
@@ -116,61 +115,61 @@ class AuthenticationUsageContribution implements IWorkbenchContribution {
}
}
class AuthenticationExtensionsContribution extends Disposable implements IWorkbenchContribution {
static ID = 'workbench.contrib.authenticationExtensions';
// class AuthenticationExtensionsContribution extends Disposable implements IWorkbenchContribution {
// static ID = 'workbench.contrib.authenticationExtensions';
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
@IAuthenticationQueryService private readonly _authenticationQueryService: IAuthenticationQueryService,
@IAuthenticationService private readonly _authenticationService: IAuthenticationService
) {
super();
void this.run();
this._register(this._extensionService.onDidChangeExtensions(this._onDidChangeExtensions, this));
this._register(
Event.any(
this._authenticationService.onDidChangeDeclaredProviders,
this._authenticationService.onDidRegisterAuthenticationProvider
)(() => this._cleanupRemovedExtensions())
);
}
// constructor(
// @IExtensionService private readonly _extensionService: IExtensionService,
// @IAuthenticationQueryService private readonly _authenticationQueryService: IAuthenticationQueryService,
// @IAuthenticationService private readonly _authenticationService: IAuthenticationService
// ) {
// super();
// void this.run();
// this._register(this._extensionService.onDidChangeExtensions(this._onDidChangeExtensions, this));
// this._register(
// Event.any(
// this._authenticationService.onDidChangeDeclaredProviders,
// this._authenticationService.onDidRegisterAuthenticationProvider
// )(() => this._cleanupRemovedExtensions())
// );
// }
async run(): Promise<void> {
await this._extensionService.whenInstalledExtensionsRegistered();
this._cleanupRemovedExtensions();
}
// async run(): Promise<void> {
// await this._extensionService.whenInstalledExtensionsRegistered();
// this._cleanupRemovedExtensions();
// }
private _onDidChangeExtensions(delta: { readonly added: readonly IExtensionDescription[]; readonly removed: readonly IExtensionDescription[] }): void {
if (delta.removed.length > 0) {
this._cleanupRemovedExtensions(delta.removed);
}
}
// private _onDidChangeExtensions(delta: { readonly added: readonly IExtensionDescription[]; readonly removed: readonly IExtensionDescription[] }): void {
// if (delta.removed.length > 0) {
// this._cleanupRemovedExtensions(delta.removed);
// }
// }
private _cleanupRemovedExtensions(removedExtensions?: readonly IExtensionDescription[]): void {
const extensionIdsToRemove = removedExtensions
? new Set(removedExtensions.map(e => e.identifier.value))
: new Set(this._extensionService.extensions.map(e => e.identifier.value));
// private _cleanupRemovedExtensions(removedExtensions?: readonly IExtensionDescription[]): void {
// const extensionIdsToRemove = removedExtensions
// ? new Set(removedExtensions.map(e => e.identifier.value))
// : new Set(this._extensionService.extensions.map(e => e.identifier.value));
// If we are cleaning up specific removed extensions, we only remove those.
const isTargetedCleanup = !!removedExtensions;
// // If we are cleaning up specific removed extensions, we only remove those.
// const isTargetedCleanup = !!removedExtensions;
const providerIds = this._authenticationQueryService.getProviderIds();
for (const providerId of providerIds) {
this._authenticationQueryService.provider(providerId).forEachAccount(account => {
account.extensions().forEach(extension => {
const shouldRemove = isTargetedCleanup
? extensionIdsToRemove.has(extension.extensionId)
: !extensionIdsToRemove.has(extension.extensionId);
// const providerIds = this._authenticationQueryService.getProviderIds();
// for (const providerId of providerIds) {
// this._authenticationQueryService.provider(providerId).forEachAccount(account => {
// account.extensions().forEach(extension => {
// const shouldRemove = isTargetedCleanup
// ? extensionIdsToRemove.has(extension.extensionId)
// : !extensionIdsToRemove.has(extension.extensionId);
if (shouldRemove) {
extension.removeUsage();
extension.setAccessAllowed(false);
}
});
});
}
}
}
// if (shouldRemove) {
// extension.removeUsage();
// extension.setAccessAllowed(false);
// }
// });
// });
// }
// }
// }
class AuthenticationMcpContribution extends Disposable implements IWorkbenchContribution {
static ID = 'workbench.contrib.authenticationMcp';
@@ -216,5 +215,5 @@ class AuthenticationMcpContribution extends Disposable implements IWorkbenchCont
registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(AuthenticationUsageContribution.ID, AuthenticationUsageContribution, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(AuthenticationExtensionsContribution.ID, AuthenticationExtensionsContribution, WorkbenchPhase.Eventually);
// registerWorkbenchContribution2(AuthenticationExtensionsContribution.ID, AuthenticationExtensionsContribution, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(AuthenticationMcpContribution.ID, AuthenticationMcpContribution, WorkbenchPhase.Eventually);
@@ -643,6 +643,12 @@ export class CreateRemoteAgentJobAction extends Action2 {
chatModel.acceptResponseProgress(addedRequest, { content, kind: 'markdownContent' });
chatModel.setResponse(addedRequest, {});
chatModel.completeResponse(addedRequest);
// Clear chat (start a new chat)
if (resultMarkdown) {
widget.clear();
}
} finally {
remoteJobCreatingKey.set(false);
}
@@ -262,9 +262,8 @@ export function registerChatTitleActions() {
chatService.resendRequest(request!, {
userSelectedModelId: languageModelId,
userSelectedTools: widget?.getUserSelectedTools(),
attempt: (request?.attempt ?? -1) + 1,
mode: widget?.input.currentModeKind,
...widget?.getModeRequestOptions(),
});
}
});
@@ -17,6 +17,7 @@ import { IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js';
import { IChatResponseModel } from '../common/chatModel.js';
import { IParsedChatRequest } from '../common/chatParserTypes.js';
import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js';
import { IChatSendRequestOptions } from '../common/chatService.js';
import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel } from '../common/chatViewModel.js';
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
import { ChatAttachmentModel } from './chatAttachmentModel.js';
@@ -208,7 +209,7 @@ export interface IChatWidget {
focusLastMessage(): void;
focusInput(): void;
hasInputFocus(): boolean;
getUserSelectedTools(): Record<string, boolean> | undefined;
getModeRequestOptions(): Partial<IChatSendRequestOptions>;
getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined;
getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[];
getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[];
@@ -56,7 +56,7 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont
const widget = chatWidgetService.getWidgetBySessionId(element.sessionId);
options.userSelectedModelId = widget?.input.currentLanguageModel;
options.mode = widget?.input.currentModeKind;
options.userSelectedTools = widget?.getUserSelectedTools();
Object.assign(options, widget?.getModeRequestOptions());
if (await this.chatService.sendRequest(element.sessionId, prompt, options)) {
confirmation.isUsed = true;
@@ -61,8 +61,7 @@ export class ChatErrorConfirmationContentPart extends Disposable implements ICha
options.confirmation = buttonData.label;
const widget = chatWidgetService.getWidgetBySessionId(element.sessionId);
options.userSelectedModelId = widget?.input.currentLanguageModel;
options.userSelectedTools = widget?.getUserSelectedTools();
options.mode = widget?.input.currentModeKind;
Object.assign(options, widget?.getModeRequestOptions());
if (await chatService.sendRequest(element.sessionId, prompt, options)) {
this._onDidChangeHeight.fire();
}
@@ -124,7 +124,7 @@ export class ChatSelectedTools extends Disposable {
let currentMap = this._sessionStates.get(currentMode.id);
let defaultEnablement = false;
if (!currentMap && currentMode.kind === ChatModeKind.Agent && currentMode.customTools) {
currentMap = this._toolsService.toToolAndToolSetEnablementMap(new Set(currentMode.customTools.read(r)));
currentMap = this._toolsService.toToolAndToolSetEnablementMap(currentMode.customTools.read(r));
}
if (!currentMap) {
currentMap = this._selectedTools.read(r);
@@ -327,9 +327,9 @@ class SetupAgent extends Disposable implements IChatAgentImplementation {
}
await chatService.resendRequest(requestModel, {
...widget?.getModeRequestOptions(),
mode,
userSelectedModelId: languageModel,
userSelectedTools: widget?.getUserSelectedTools()
});
}
@@ -185,6 +185,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
private isRequestPaused: IContextKey<boolean>;
private canRequestBePaused: IContextKey<boolean>;
private agentInInput: IContextKey<boolean>;
private currentRequest: Promise<void> | undefined;
private _visible = false;
@@ -1626,6 +1627,11 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId);
if (this.currentRequest) {
// We have to wait the current request to be properly cancelled so that it has a chance to update the model with its result metadata.
// This is awkward, it's basically a limitation of the chat provider-based agent.
await Promise.race([this.currentRequest, timeout(1000)]);
}
this.input.validateAgentMode();
@@ -1640,21 +1646,20 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
const result = await this.chatService.sendRequest(this.viewModel.sessionId, requestInputs.input, {
mode: this.input.currentModeKind,
userSelectedModelId: this.input.currentLanguageModel,
location: this.location,
locationData: this._location.resolveData?.(),
parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind },
attachedContext: requestInputs.attachedContext.asArray(),
noCommandDetection: options?.noCommandDetection,
userSelectedTools: this.getUserSelectedTools(),
...this.getModeRequestOptions(),
modeInstructions: this.input.currentModeObs.get().body?.get()
});
if (result) {
this.input.acceptInput(isUserQuery);
this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand });
result.responseCompletePromise.then(() => {
this.currentRequest = result.responseCompletePromise.then(() => {
const responses = this.viewModel?.getItems().filter(isResponseVM);
const lastResponse = responses?.[responses.length - 1];
this.chatAccessibilityService.acceptResponse(lastResponse, requestId, options?.isVoiceInput);
@@ -1665,6 +1670,8 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.input.setValue(question, false);
}
}
this.currentRequest = undefined;
});
if (this.viewModel?.editing) {
@@ -1909,7 +1916,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
// if not tools to enable are present, we are done
if (tools !== undefined && this.input.currentModeKind === ChatModeKind.Agent) {
const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(new Set(tools));
const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(tools);
this.input.selectedToolsModel.set(enablementMap, true);
}
@@ -484,13 +484,19 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
return result;
}
toToolAndToolSetEnablementMap(toolOrToolSetNames: Set<string>): Map<ToolSet | IToolData, boolean> {
/**
* Create a map that contains all tools and toolsets with their enablement state.
* @param toolOrToolSetNames A list of tool or toolset names to check for enablement. If undefined, all tools and toolsets are enabled.
* @returns A map of tool or toolset instances to their enablement state.
*/
toToolAndToolSetEnablementMap(enabledToolOrToolSetNames: readonly string[] | undefined): Map<ToolSet | IToolData, boolean> {
const toolOrToolSetNames = enabledToolOrToolSetNames ? new Set(enabledToolOrToolSetNames) : undefined;
const result = new Map<ToolSet | IToolData, boolean>();
for (const tool of this._tools.values()) {
result.set(tool.data, tool.data.toolReferenceName !== undefined && toolOrToolSetNames.has(tool.data.toolReferenceName));
result.set(tool.data, tool.data.toolReferenceName !== undefined && (toolOrToolSetNames === undefined || toolOrToolSetNames.has(tool.data.toolReferenceName)));
}
for (const toolSet of this._toolSets) {
result.set(toolSet, toolOrToolSetNames.has(toolSet.referenceName));
result.set(toolSet, (toolOrToolSetNames === undefined || toolOrToolSetNames.has(toolSet.referenceName)));
}
return result;
}
@@ -78,7 +78,7 @@ class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider
private async updateTools(model: ITextModel, tools: PromptToolsMetadata) {
const selectedToolsNow = tools.value ? this.languageModelToolsService.toToolAndToolSetEnablementMap(new Set(tools.value)) : new Map();
const selectedToolsNow = tools.value ? this.languageModelToolsService.toToolAndToolSetEnablementMap(tools.value) : new Map();
const newSelectedAfter = await this.instantiationService.invokeFunction(showToolsPicker, localize('placeholder', "Select tools"), undefined, selectedToolsNow);
if (!newSelectedAfter) {
return;
@@ -185,7 +185,7 @@ export class ChatModeService extends Disposable implements IChatModeService {
];
if (this.chatAgentService.hasToolsAgent) {
builtinModes.push(ChatMode.Agent);
builtinModes.unshift(ChatMode.Agent);
}
builtinModes.push(ChatMode.Edit);
return builtinModes;
@@ -275,7 +275,7 @@ export interface ILanguageModelToolsService {
resetToolAutoConfirmation(): void;
cancelToolCallsForRequest(requestId: string): void;
toToolEnablementMap(toolOrToolSetNames: Set<string>): Record<string, boolean>;
toToolAndToolSetEnablementMap(toolOrToolSetNames: Set<string>): IToolAndToolSetEnablementMap;
toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[] | undefined): IToolAndToolSetEnablementMap;
readonly toolSets: IObservable<Iterable<ToolSet>>;
getToolSet(id: string): ToolSet | undefined;
@@ -23,6 +23,10 @@ import { resolve } from '../../../../base/common/path.js';
import { showChatView } from '../browser/chat.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ViewContainerLocation } from '../../../common/views.js';
class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution {
@@ -49,7 +53,9 @@ class ChatCommandLineHandler extends Disposable {
@ICommandService private readonly commandService: ICommandService,
@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,
@IViewsService private readonly viewsService: IViewsService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IContextKeyService private readonly contextKeyService: IContextKeyService
) {
super();
@@ -84,6 +90,16 @@ class ChatCommandLineHandler extends Disposable {
};
const chatWidget = await showChatView(this.viewsService);
if (args.maximize) {
const location = this.contextKeyService.getContextKeyValue<ViewContainerLocation>(ChatContextKeys.panelLocation.key);
if (location === ViewContainerLocation.AuxiliaryBar) {
this.layoutService.setAuxiliaryBarMaximized(true);
} else if (location === ViewContainerLocation.Panel && !this.layoutService.isPanelMaximized()) {
this.layoutService.toggleMaximizedPanel();
}
}
await chatWidget?.waitForReady();
await this.commandService.executeCommand(ACTION_ID_NEW_CHAT);
await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, opts);
@@ -76,7 +76,7 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService
throw new Error('Method not implemented.');
}
toToolAndToolSetEnablementMap(toolOrToolSetNames: Set<string>): Map<ToolSet | IToolData, boolean> {
toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[] | undefined): Map<ToolSet | IToolData, boolean> {
throw new Error('Method not implemented.');
}
}
@@ -12,7 +12,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { manageExtensionIcon } from '../../extensions/browser/extensionsIcons.js';
import { getDomNodePagePosition } from '../../../../base/browser/dom.js';
import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState, McpServerEditorTab } from '../common/mcpTypes.js';
import { IMcpSamplingService, IMcpServer, IMcpServerContainer, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCapability, McpConnectionState, McpServerEditorTab, McpServerInstallState } from '../common/mcpTypes.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { McpCommandIds } from '../common/mcpCommandIds.js';
@@ -116,6 +116,9 @@ export class InstallAction extends McpServerAction {
if (!this.mcpServer?.gallery && !this.mcpServer?.installable) {
return;
}
if (this.mcpServer.installState !== McpServerInstallState.Uninstalled) {
return;
}
this.class = InstallAction.CLASS;
this.enabled = true;
this.label = localize('install', "Install");
@@ -145,6 +148,20 @@ export class InstallAction extends McpServerAction {
}
}
export class InstallingLabelAction extends McpServerAction {
private static readonly LABEL = localize('installing', "Installing");
private static readonly CLASS = `${McpServerAction.LABEL_ACTION_CLASS} install installing`;
constructor() {
super('extension.installing', InstallingLabelAction.LABEL, InstallingLabelAction.CLASS, false);
}
update(): void {
this.class = `${InstallingLabelAction.CLASS}${this.mcpServer && this.mcpServer.installState === McpServerInstallState.Installing ? '' : ' hide'}`;
}
}
export class UninstallAction extends McpServerAction {
static readonly CLASS = `${this.LABEL_ACTION_CLASS} prominent uninstall`;
@@ -166,6 +183,10 @@ export class UninstallAction extends McpServerAction {
if (!this.mcpServer.local) {
return;
}
if (this.mcpServer.installState !== McpServerInstallState.Installed) {
this.enabled = false;
return;
}
this.class = UninstallAction.CLASS;
this.enabled = true;
this.label = localize('uninstall', "Uninstall");
@@ -39,7 +39,7 @@ import { IExtensionService } from '../../../services/extensions/common/extension
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { IMcpServerEditorOptions, IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js';
import { InstallCountWidget, McpServerIconWidget, McpServerWidget, onClick, PublisherWidget, RatingsWidget } from './mcpServerWidgets.js';
import { DropDownAction, InstallAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js';
import { DropDownAction, InstallAction, InstallingLabelAction, ManageMcpServerAction, UninstallAction } from './mcpServerActions.js';
import { McpServerEditorInput } from './mcpServerEditorInput.js';
import { ILocalMcpServer, IMcpServerManifest, IMcpServerPackage, PackageType } from '../../../../platform/mcp/common/mcpManagement.js';
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
@@ -220,6 +220,7 @@ export class McpServerEditor extends EditorPane {
const actions = [
this.instantiationService.createInstance(InstallAction, true),
this.instantiationService.createInstance(InstallingLabelAction),
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(ManageMcpServerAction, true),
];
@@ -25,7 +25,7 @@ import { getLocationBasedViewColors, ViewPane } from '../../../browser/parts/vie
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
import { IViewDescriptorService, IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js';
import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon, McpServerInstallState } from '../common/mcpTypes.js';
import { DropDownAction, InstallAction, ManageMcpServerAction } from './mcpServerActions.js';
import { DropDownAction, InstallAction, InstallingLabelAction, ManageMcpServerAction } from './mcpServerActions.js';
import { PublisherWidget, InstallCountWidget, RatingsWidget, McpServerIconWidget } from './mcpServerWidgets.js';
import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
@@ -321,6 +321,7 @@ class McpServerRenderer implements IListRenderer<IWorkbenchMcpServer, IMcpServer
const actions = [
this.instantiationService.createInstance(InstallAction, false),
this.instantiationService.createInstance(InstallingLabelAction),
this.instantiationService.createInstance(ManageMcpServerAction, false),
];
@@ -907,6 +907,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}
hideSuggestWidget(cancelAnyRequest: boolean): void {
this._discoverability?.resetTimer();
if (cancelAnyRequest) {
this._cancellationTokenSource?.cancel();
this._cancellationTokenSource = undefined;
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TimeoutTimer } from '../../../../../base/common/async.js';
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
@@ -23,7 +24,7 @@ interface ITerminalSuggestShownTracker extends IDisposable {
export class TerminalSuggestShownTracker extends Disposable implements ITerminalSuggestShownTracker {
private _done: boolean;
private _count: number;
private _timeout: Timeout | undefined;
private _timeout: TimeoutTimer | undefined;
private _start: number | undefined;
private _firstShownTracker: { shell: Set<TerminalShellType>; window: boolean } | undefined = undefined;
@@ -51,6 +52,14 @@ export class TerminalSuggestShownTracker extends Disposable implements ITerminal
this._firstShownTracker = undefined;
}
resetTimer(): void {
if (this._timeout) {
this._timeout.cancel();
this._timeout = undefined;
}
this._start = undefined;
}
update(widgetElt: HTMLElement | undefined): void {
if (this._done) {
return;
@@ -63,10 +72,11 @@ export class TerminalSuggestShownTracker extends Disposable implements ITerminal
if (this._count >= TERMINAL_SUGGEST_DISCOVERABILITY_MAX_COUNT) {
this._setDone(widgetElt);
} else if (!this._start) {
this.resetTimer();
this._start = Date.now();
this._timeout = setTimeout(() => {
this._timeout = this._register(new TimeoutTimer(() => {
this._setDone(widgetElt);
}, TERMINAL_SUGGEST_DISCOVERABILITY_MIN_MS);
}, TERMINAL_SUGGEST_DISCOVERABILITY_MIN_MS));
}
}
@@ -77,7 +87,7 @@ export class TerminalSuggestShownTracker extends Disposable implements ITerminal
widgetElt.classList.remove('increased-discoverability');
}
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout.cancel();
this._timeout = undefined;
}
this._start = undefined;