From 676ae78fa5f71398c50e42f887f5b0da052d9ec8 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 15 Nov 2025 13:12:37 -0800 Subject: [PATCH] Fix localized tool markdownDescriptions (#277589) * Fix localized tool markdownDescriptions And add a lint rule * Just keep this the same * Fixes --- .../code-no-localized-model-description.ts | 128 ++++++++++++++++++ eslint.config.js | 1 + .../contrib/chat/browser/chatSetup.ts | 2 +- .../tools/languageModelToolsContribution.ts | 1 + .../electron-browser/tools/fetchPageTool.ts | 2 +- .../common/installExtensionsTool.ts | 2 +- .../extensions/common/searchExtensionsTool.ts | 2 +- .../tools/task/createAndRunTaskTool.ts | 4 +- 8 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 .eslint-plugin-local/code-no-localized-model-description.ts diff --git a/.eslint-plugin-local/code-no-localized-model-description.ts b/.eslint-plugin-local/code-no-localized-model-description.ts new file mode 100644 index 00000000000..a624aeb8619 --- /dev/null +++ b/.eslint-plugin-local/code-no-localized-model-description.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TSESTree } from '@typescript-eslint/utils'; +import * as eslint from 'eslint'; +import * as visitorKeys from 'eslint-visitor-keys'; +import type * as ESTree from 'estree'; + +const MESSAGE_ID = 'noLocalizedModelDescription'; +type NodeWithChildren = TSESTree.Node & { + [key: string]: TSESTree.Node | TSESTree.Node[] | null | undefined; +}; +type PropertyKeyNode = TSESTree.Property['key'] | TSESTree.MemberExpression['property']; +type AssignmentTarget = TSESTree.AssignmentExpression['left']; + +export default new class NoLocalizedModelDescriptionRule implements eslint.Rule.RuleModule { + meta: eslint.Rule.RuleMetaData = { + messages: { + [MESSAGE_ID]: 'modelDescription values describe behavior to the language model and must not use localized strings.' + }, + type: 'problem', + schema: false + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + const reportIfLocalized = (expression: TSESTree.Expression | null | undefined) => { + if (expression && containsLocalizedCall(expression)) { + context.report({ node: expression, messageId: MESSAGE_ID }); + } + }; + + return { + Property: (node: ESTree.Property) => { + const propertyNode = node as TSESTree.Property; + if (!isModelDescriptionKey(propertyNode.key, propertyNode.computed)) { + return; + } + reportIfLocalized(propertyNode.value as TSESTree.Expression); + }, + AssignmentExpression: (node: ESTree.AssignmentExpression) => { + const assignment = node as TSESTree.AssignmentExpression; + if (!isModelDescriptionAssignmentTarget(assignment.left)) { + return; + } + reportIfLocalized(assignment.right); + } + }; + } +}; + +function isModelDescriptionKey(key: PropertyKeyNode, computed: boolean | undefined): boolean { + if (!computed && key.type === 'Identifier') { + return key.name === 'modelDescription'; + } + if (key.type === 'Literal' && key.value === 'modelDescription') { + return true; + } + return false; +} + +function isModelDescriptionAssignmentTarget(target: AssignmentTarget): target is TSESTree.MemberExpression { + if (target.type === 'MemberExpression') { + return isModelDescriptionKey(target.property, target.computed); + } + return false; +} + +function containsLocalizedCall(expression: TSESTree.Expression): boolean { + let found = false; + + const visit = (node: TSESTree.Node) => { + if (found) { + return; + } + + if (isLocalizeCall(node)) { + found = true; + return; + } + + for (const key of visitorKeys.KEYS[node.type] ?? []) { + const value = (node as NodeWithChildren)[key]; + if (Array.isArray(value)) { + for (const child of value) { + if (child) { + visit(child); + if (found) { + return; + } + } + } + } else if (value) { + visit(value); + } + } + }; + + visit(expression); + return found; +} + +function isLocalizeCall(node: TSESTree.Node): boolean { + if (node.type === 'CallExpression') { + return isLocalizeCallee(node.callee); + } + if (node.type === 'ChainExpression') { + return isLocalizeCall(node.expression); + } + return false; +} + + +function isLocalizeCallee(callee: TSESTree.CallExpression['callee']): boolean { + if (callee.type === 'Identifier') { + return callee.name === 'localize'; + } + if (callee.type === 'MemberExpression') { + if (!callee.computed && callee.property.type === 'Identifier') { + return callee.property.name === 'localize'; + } + if (callee.property.type === 'Literal' && callee.property.value === 'localize') { + return true; + } + } + return false; +} diff --git a/eslint.config.js b/eslint.config.js index 69729bcdfb4..009545960ad 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -89,6 +89,7 @@ export default tseslint.config( 'local/code-declare-service-brand': 'warn', 'local/code-no-reader-after-await': 'warn', 'local/code-no-observable-get-in-reactive-context': 'warn', + 'local/code-no-localized-model-description': 'warn', 'local/code-policy-localization-key-match': 'warn', 'local/code-no-localization-template-literals': 'error', 'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }], diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 0c4064dd961..b928f03a712 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -178,7 +178,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { source: ToolDataSource.Internal, icon: Codicon.newFolder, displayName: localize('setupToolDisplayName', "New Workspace"), - modelDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), + modelDescription: 'Scaffold a new workspace in VS Code', userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), canBeReferencedInPrompt: true, toolReferenceName: 'new', diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index b580f5e4cc0..2569bdf51cf 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -86,6 +86,7 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r description: localize('toolUserDescription', "A description of this tool that may be shown to the user."), type: 'string' }, + // eslint-disable-next-line local/code-no-localized-model-description modelDescription: { description: localize('toolModelDescription', "A description of this tool that may be used by a language model to select it."), type: 'string' diff --git a/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts index 5eabbedd2e3..2cc132853a0 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts @@ -25,7 +25,7 @@ export const FetchWebPageToolData: IToolData = { id: InternalFetchWebPageToolId, displayName: 'Fetch Web Page', canBeReferencedInPrompt: false, - modelDescription: localize('fetchWebPage.modelDescription', 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.'), + modelDescription: 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.', source: ToolDataSource.Internal, canRequestPostApproval: true, canRequestPreApproval: true, diff --git a/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts index 96808f4f361..b893c621156 100644 --- a/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts @@ -17,7 +17,7 @@ export const InstallExtensionsToolData: IToolData = { toolReferenceName: 'installExtensions', canBeReferencedInPrompt: true, displayName: localize('installExtensionsTool.displayName', 'Install Extensions'), - modelDescription: localize('installExtensionsTool.modelDescription', "This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install. The identifier of an extension is '\${ publisher }.\${ name }' for example: 'vscode.csharp'."), + modelDescription: 'This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install. The identifier of an extension is \'\${ publisher }.\${ name }\' for example: \'vscode.csharp\'.', userDescription: localize('installExtensionsTool.userDescription', 'Tool for installing extensions'), source: ToolDataSource.Internal, inputSchema: { diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index 4d803b9f6f7..0871add7ce9 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -20,7 +20,7 @@ export const SearchExtensionsToolData: IToolData = { canBeReferencedInPrompt: true, icon: ThemeIcon.fromId(Codicon.extensions.id), displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), - modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."), + modelDescription: 'This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended.', userDescription: localize('searchExtensionsTool.userDescription', 'Search for VS Code extensions'), source: ToolDataSource.Internal, inputSchema: { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts index 2620dab3810..280ba5f1f29 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/task/createAndRunTaskTool.ts @@ -194,7 +194,7 @@ export const CreateAndRunTaskToolData: IToolData = { id: 'create_and_run_task', toolReferenceName: 'createAndRunTask', displayName: localize('createAndRunTask.displayName', 'Create and run Task'), - modelDescription: localize('createAndRunTask.modelDescription', 'Creates and runs a build, run, or custom task for the workspace by generating or adding to a tasks.json file based on the project structure (such as package.json or README.md). If the user asks to build, run, launch and they have no tasks.json file, use this tool. If they ask to create or add a task, use this tool.'), + modelDescription: 'Creates and runs a build, run, or custom task for the workspace by generating or adding to a tasks.json file based on the project structure (such as package.json or README.md). If the user asks to build, run, launch and they have no tasks.json file, use this tool. If they ask to create or add a task, use this tool.', userDescription: localize('createAndRunTask.userDescription', "Create and run a task in the workspace"), source: ToolDataSource.Internal, inputSchema: { @@ -259,5 +259,3 @@ export const CreateAndRunTaskToolData: IToolData = { ] }, }; - -