From 856d8bf6ee36df24507fbbacdb54cbf40545b412 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:51:28 -0800 Subject: [PATCH 01/25] Auto approve most rg calls by default Fixes #282824 --- .../common/terminalChatAgentToolsConfiguration.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index afbeb57cc74..3489dce3517 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -260,6 +260,12 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Thu, 11 Dec 2025 15:16:32 -0800 Subject: [PATCH 02/25] Add rg approve tests --- .../test/electron-browser/runInTerminalTool.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 233f5eb5d95..73bd49bd9b0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -243,6 +243,9 @@ suite('RunInTerminalTool', () => { 'date +%Y-%m-%d', 'find . -name "*.txt"', 'grep pattern file.txt', + 'rg pattern file.txt', + 'rg --json pattern .', + 'rg -i --color=never "TODO" src/', 'sort file.txt', 'tree directory' ]; @@ -295,6 +298,8 @@ suite('RunInTerminalTool', () => { 'find . -exec rm {} \\;', 'find . -execdir rm {} \\;', 'find . -fprint output.txt', + 'rg --pre cat pattern .', + 'rg --hostname-bin hostname pattern .', 'sort -o /etc/passwd file.txt', 'sort -S 100G file.txt', 'tree -o output.txt', From eefd7a2d0f7f1dc6581e80418331fd5a8dea9a15 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:56:30 -0800 Subject: [PATCH 03/25] Add default auto approve rule for sed Fixes #282209 --- .../terminalChatAgentToolsConfiguration.ts | 16 ++++++++++++++++ .../electron-browser/runInTerminalTool.test.ts | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index afbeb57cc74..44673713c3c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -260,6 +260,22 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { 'date +%Y-%m-%d', 'find . -name "*.txt"', 'grep pattern file.txt', + 'sed "s/foo/bar/g"', + 'sed -n "1,10p" file.txt', 'sort file.txt', 'tree directory' ]; @@ -295,6 +297,16 @@ suite('RunInTerminalTool', () => { 'find . -exec rm {} \\;', 'find . -execdir rm {} \\;', 'find . -fprint output.txt', + 'sed -i "s/foo/bar/g" file.txt', + 'sed -i.bak "s/foo/bar/" file.txt', + 'sed --in-place "s/foo/bar/" file.txt', + 'sed -e "s/a/b/" file.txt', + 'sed -f script.sed file.txt', + 'sed --expression "s/a/b/" file.txt', + 'sed --file script.sed file.txt', + 'sed "s/foo/bar/e" file.txt', + 'sed "s/foo/bar/w output.txt" file.txt', + 'sed ";W output.txt" file.txt', 'sort -o /etc/passwd file.txt', 'sort -S 100G file.txt', 'tree -o output.txt', From c348a9a82b9a62377b4c5a9065ccbe4bda6048ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:24:14 -0800 Subject: [PATCH 04/25] Move npm spec out of upstream Part of #284593 --- .../src/completions/{upstream => }/npm.ts | 140 +++++++++--------- extensions/terminal-suggest/src/constants.ts | 1 - .../src/terminalSuggestMain.ts | 2 + 3 files changed, 72 insertions(+), 71 deletions(-) rename extensions/terminal-suggest/src/completions/{upstream => }/npm.ts (94%) diff --git a/extensions/terminal-suggest/src/completions/upstream/npm.ts b/extensions/terminal-suggest/src/completions/npm.ts similarity index 94% rename from extensions/terminal-suggest/src/completions/upstream/npm.ts rename to extensions/terminal-suggest/src/completions/npm.ts index aa142e05661..6519aca4091 100644 --- a/extensions/terminal-suggest/src/completions/upstream/npm.ts +++ b/extensions/terminal-suggest/src/completions/npm.ts @@ -16,82 +16,82 @@ const atsInStr = (s: string) => (s.match(/@/g) || []).length; export const createNpmSearchHandler = (keywords?: string[]) => - async ( - context: string[], - executeShellCommand: Fig.ExecuteCommandFunction, - shellContext: Fig.ShellContext - ): Promise => { - const searchTerm = context[context.length - 1]; - if (searchTerm === "") { - return []; - } - // Add optional keyword parameter - const keywordParameter = - keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; + async ( + context: string[], + executeShellCommand: Fig.ExecuteCommandFunction, + shellContext: Fig.ShellContext + ): Promise => { + const searchTerm = context[context.length - 1]; + if (searchTerm === "") { + return []; + } + // Add optional keyword parameter + const keywordParameter = + keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; - const queryPackagesUrl = keywordParameter - ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` - : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; + const queryPackagesUrl = keywordParameter + ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` + : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; - // Query the API with the package name - const queryPackages = [ - "-s", - "-H", - "Accept: application/json", - queryPackagesUrl, - ]; - // We need to remove the '@' at the end of the searchTerm before querying versions - const queryVersions = [ - "-s", - "-H", - "Accept: application/vnd.npm.install-v1+json", - `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, - ]; - // If the end of our token is '@', then we want to generate version suggestions - // Otherwise, we want packages - const out = (query: string) => - executeShellCommand({ - command: "curl", - args: query[query.length - 1] === "@" ? queryVersions : queryPackages, - }); - // If our token starts with '@', then a 2nd '@' tells us we want - // versions. - // Otherwise, '@' anywhere else in the string will indicate the same. - const shouldGetVersion = searchTerm.startsWith("@") - ? atsInStr(searchTerm) > 1 - : searchTerm.includes("@"); + // Query the API with the package name + const queryPackages = [ + "-s", + "-H", + "Accept: application/json", + queryPackagesUrl, + ]; + // We need to remove the '@' at the end of the searchTerm before querying versions + const queryVersions = [ + "-s", + "-H", + "Accept: application/vnd.npm.install-v1+json", + `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, + ]; + // If the end of our token is '@', then we want to generate version suggestions + // Otherwise, we want packages + const out = (query: string) => + executeShellCommand({ + command: "curl", + args: query[query.length - 1] === "@" ? queryVersions : queryPackages, + }); + // If our token starts with '@', then a 2nd '@' tells us we want + // versions. + // Otherwise, '@' anywhere else in the string will indicate the same. + const shouldGetVersion = searchTerm.startsWith("@") + ? atsInStr(searchTerm) > 1 + : searchTerm.includes("@"); - try { - const data = JSON.parse((await out(searchTerm)).stdout); - if (shouldGetVersion) { - // create dist tags suggestions - const versions = Object.entries(data["dist-tags"] || {}).map( - ([key, value]) => ({ - name: key, - description: value, + try { + const data = JSON.parse((await out(searchTerm)).stdout); + if (shouldGetVersion) { + // create dist tags suggestions + const versions = Object.entries(data["dist-tags"] || {}).map( + ([key, value]) => ({ + name: key, + description: value, + }) + ) as Fig.Suggestion[]; + // create versions + versions.push( + ...Object.keys(data.versions) + .map((version) => ({ name: version }) as Fig.Suggestion) + .reverse() + ); + return versions; + } + + const results = keywordParameter ? data.results : data; + return results.map( + (item: { package: { name: string; description: string } }) => ({ + name: item.package.name, + description: item.package.description, }) ) as Fig.Suggestion[]; - // create versions - versions.push( - ...Object.keys(data.versions) - .map((version) => ({ name: version }) as Fig.Suggestion) - .reverse() - ); - return versions; + } catch (error) { + console.error({ error }); + return []; } - - const results = keywordParameter ? data.results : data; - return results.map( - (item: { package: { name: string; description: string } }) => ({ - name: item.package.name, - description: item.package.description, - }) - ) as Fig.Suggestion[]; - } catch (error) { - console.error({ error }); - return []; - } - }; + }; // GENERATORS export const npmSearchGenerator: Fig.Generator = { diff --git a/extensions/terminal-suggest/src/constants.ts b/extensions/terminal-suggest/src/constants.ts index 7d877e73961..086d7ca8672 100644 --- a/extensions/terminal-suggest/src/constants.ts +++ b/extensions/terminal-suggest/src/constants.ts @@ -113,7 +113,6 @@ export const upstreamSpecs = [ // JavaScript / TypeScript 'node', - 'npm', 'nvm', 'pnpm', 'yarn', diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 3087b8ad258..774a33f07da 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -16,6 +16,7 @@ import codeTunnelInsidersCompletionSpec from './completions/code-tunnel-insiders import copilotSpec from './completions/copilot'; import gitCompletionSpec from './completions/git'; import ghCompletionSpec from './completions/gh'; +import npmCompletionSpec from './completions/npm'; import npxCompletionSpec from './completions/npx'; import setLocationSpec from './completions/set-location'; import { upstreamSpecs } from './constants'; @@ -69,6 +70,7 @@ export const availableSpecs: Fig.Spec[] = [ copilotSpec, gitCompletionSpec, ghCompletionSpec, + npmCompletionSpec, npxCompletionSpec, setLocationSpec, ]; From e0e5e8b489a935bd4299d69333bcb7036b902338 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:24:41 -0800 Subject: [PATCH 05/25] Replace double with single quotes --- .../terminal-suggest/src/completions/npm.ts | 1163 +++++++++-------- 1 file changed, 582 insertions(+), 581 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/npm.ts b/extensions/terminal-suggest/src/completions/npm.ts index 6519aca4091..111d502482c 100644 --- a/extensions/terminal-suggest/src/completions/npm.ts +++ b/extensions/terminal-suggest/src/completions/npm.ts @@ -1,11 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + function uninstallSubcommand(named: string | string[]): Fig.Subcommand { return { name: named, - description: "Uninstall a package", + description: 'Uninstall a package', args: { - name: "package", + name: 'package', generators: dependenciesGenerator, - filterStrategy: "fuzzy", + filterStrategy: 'fuzzy', isVariadic: true, }, options: npmUninstallOptions, @@ -22,12 +27,12 @@ export const createNpmSearchHandler = shellContext: Fig.ShellContext ): Promise => { const searchTerm = context[context.length - 1]; - if (searchTerm === "") { + if (searchTerm === '') { return []; } // Add optional keyword parameter const keywordParameter = - keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; + keywords && keywords.length > 0 ? `+keywords:${keywords.join(',')}` : ''; const queryPackagesUrl = keywordParameter ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` @@ -35,37 +40,37 @@ export const createNpmSearchHandler = // Query the API with the package name const queryPackages = [ - "-s", - "-H", - "Accept: application/json", + '-s', + '-H', + 'Accept: application/json', queryPackagesUrl, ]; // We need to remove the '@' at the end of the searchTerm before querying versions const queryVersions = [ - "-s", - "-H", - "Accept: application/vnd.npm.install-v1+json", + '-s', + '-H', + 'Accept: application/vnd.npm.install-v1+json', `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, ]; // If the end of our token is '@', then we want to generate version suggestions // Otherwise, we want packages const out = (query: string) => executeShellCommand({ - command: "curl", - args: query[query.length - 1] === "@" ? queryVersions : queryPackages, + command: 'curl', + args: query[query.length - 1] === '@' ? queryVersions : queryPackages, }); // If our token starts with '@', then a 2nd '@' tells us we want // versions. // Otherwise, '@' anywhere else in the string will indicate the same. - const shouldGetVersion = searchTerm.startsWith("@") + const shouldGetVersion = searchTerm.startsWith('@') ? atsInStr(searchTerm) > 1 - : searchTerm.includes("@"); + : searchTerm.includes('@'); try { const data = JSON.parse((await out(searchTerm)).stdout); if (shouldGetVersion) { // create dist tags suggestions - const versions = Object.entries(data["dist-tags"] || {}).map( + const versions = Object.entries(data['dist-tags'] || {}).map( ([key, value]) => ({ name: key, description: value, @@ -74,7 +79,7 @@ export const createNpmSearchHandler = // create versions versions.push( ...Object.keys(data.versions) - .map((version) => ({ name: version }) as Fig.Suggestion) + .map((version) => ({ name: version })) .reverse() ); return versions; @@ -100,15 +105,15 @@ export const npmSearchGenerator: Fig.Generator = { // the 2nd '@' is typed because we'll need to generate version // suggetsions // e.g. @typescript-eslint/types - if (oldToken.startsWith("@")) { + if (oldToken.startsWith('@')) { return !(atsInStr(oldToken) > 1 && atsInStr(newToken) > 1); } // If the package name doesn't start with '@', then trigger when // we see the first '@' so we can generate version suggestions - return !(oldToken.includes("@") && newToken.includes("@")); + return !(oldToken.includes('@') && newToken.includes('@')); }, - getQueryTerm: "@", + getQueryTerm: '@', cache: { ttl: 1000 * 60 * 60 * 24 * 2, // 2 days }, @@ -119,31 +124,29 @@ const workspaceGenerator: Fig.Generator = { // script: "cat $(npm prefix)/package.json", custom: async (tokens, executeShellCommand) => { const { stdout: npmPrefix } = await executeShellCommand({ - command: "npm", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["prefix"], + command: 'npm', + args: ['prefix'], }); const { stdout: out } = await executeShellCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays + command: 'cat', args: [`${npmPrefix}/package.json`], }); const suggestions: Fig.Suggestion[] = []; try { - if (out.trim() == "") { + if (out.trim() === '') { return suggestions; } const packageContent = JSON.parse(out); - const workspaces = packageContent["workspaces"]; + const workspaces = packageContent['workspaces']; if (workspaces) { for (const workspace of workspaces) { suggestions.push({ name: workspace, - description: "Workspaces", + description: 'Workspaces', }); } } @@ -156,23 +159,21 @@ const workspaceGenerator: Fig.Generator = { /** Generator that lists package.json dependencies */ export const dependenciesGenerator: Fig.Generator = { - trigger: (newToken) => newToken === "-g" || newToken === "--global", + trigger: (newToken) => newToken === '-g' || newToken === '--global', custom: async function (tokens, executeShellCommand) { - if (!tokens.includes("-g") && !tokens.includes("--global")) { + if (!tokens.includes('-g') && !tokens.includes('--global')) { const { stdout: npmPrefix } = await executeShellCommand({ - command: "npm", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["prefix"], + command: 'npm', + args: ['prefix'], }); const { stdout: out } = await executeShellCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays + command: 'cat', args: [`${npmPrefix}/package.json`], }); const packageContent = JSON.parse(out); - const dependencies = packageContent["dependencies"] ?? {}; - const devDependencies = packageContent["devDependencies"]; - const optionalDependencies = packageContent["optionalDependencies"] ?? {}; + const dependencies = packageContent['dependencies'] ?? {}; + const devDependencies = packageContent['devDependencies']; + const optionalDependencies = packageContent['optionalDependencies'] ?? {}; Object.assign(dependencies, devDependencies, optionalDependencies); return Object.keys(dependencies) @@ -182,22 +183,22 @@ export const dependenciesGenerator: Fig.Generator = { }) .map((pkgName) => ({ name: pkgName, - icon: "📦", + icon: '📦', description: dependencies[pkgName] - ? "dependency" + ? 'dependency' : optionalDependencies[pkgName] - ? "optionalDependency" - : "devDependency", + ? 'optionalDependency' + : 'devDependency', })); } else { const { stdout } = await executeShellCommand({ - command: "bash", - args: ["-c", "ls -1 `npm root -g`"], + command: 'bash', + args: ['-c', 'ls -1 `npm root -g`'], }); - return stdout.split("\n").map((name) => ({ + return stdout.split('\n').map((name) => ({ name, - icon: "📦", - description: "Global dependency", + icon: '📦', + description: 'Global dependency', })); } }, @@ -206,30 +207,30 @@ export const dependenciesGenerator: Fig.Generator = { /** Generator that lists package.json scripts (with the respect to the `fig` field) */ export const npmScriptsGenerator: Fig.Generator = { cache: { - strategy: "stale-while-revalidate", + strategy: 'stale-while-revalidate', cacheByDirectory: true, }, script: [ - "bash", - "-c", - "until [[ -f package.json ]] || [[ $PWD = '/' ]]; do cd ..; done; cat package.json", + 'bash', + '-c', + 'until [[ -f package.json ]] || [[ $PWD = \' / \' ]]; do cd ..; done; cat package.json', ], postProcess: function (out, [npmClient]) { - if (out.trim() == "") { + if (out.trim() === '') { return []; } try { const packageContent = JSON.parse(out); - const scripts = packageContent["scripts"]; - const figCompletions = packageContent["fig"] || {}; + const scripts = packageContent['scripts']; + const figCompletions = packageContent['fig'] || {}; if (scripts) { return Object.entries(scripts).map(([scriptName, scriptContents]) => { const icon = - npmClient === "yarn" - ? "fig://icon?type=yarn" - : "fig://icon?type=npm"; + npmClient === 'yarn' + ? 'fig://icon?type=yarn' + : 'fig://icon?type=npm'; const customScripts: Fig.Suggestion = figCompletions[scriptName]; return { name: scriptName, @@ -253,105 +254,105 @@ export const npmScriptsGenerator: Fig.Generator = { }; const globalOption: Fig.Option = { - name: ["-g", "--global"], + name: ['-g', '--global'], description: - "Operates in 'global' mode, so that packages are installed into the prefix folder instead of the current working directory", + 'Operates in \'global\' mode, so that packages are installed into the prefix folder instead of the current working directory', }; const jsonOption: Fig.Option = { - name: "--json", - description: "Show output in json format", + name: '--json', + description: 'Show output in json format', }; const omitOption: Fig.Option = { - name: "--omit", - description: "Dependency types to omit from the installation tree on disk", + name: '--omit', + description: 'Dependency types to omit from the installation tree on disk', args: { - name: "Package type", - default: "dev", - suggestions: ["dev", "optional", "peer"], + name: 'Package type', + default: 'dev', + suggestions: ['dev', 'optional', 'peer'], }, isRepeatable: 3, }; const parseableOption: Fig.Option = { - name: ["-p", "--parseable"], + name: ['-p', '--parseable'], description: - "Output parseable results from commands that write to standard output", + 'Output parseable results from commands that write to standard output', }; const longOption: Fig.Option = { - name: ["-l", "--long"], - description: "Show extended information", + name: ['-l', '--long'], + description: 'Show extended information', }; const workSpaceOptions: Fig.Option[] = [ { - name: ["-w", "--workspace"], + name: ['-w', '--workspace'], description: - "Enable running a command in the context of the configured workspaces of the current project", + 'Enable running a command in the context of the configured workspaces of the current project', args: { - name: "workspace", + name: 'workspace', generators: workspaceGenerator, isVariadic: true, }, }, { - name: ["-ws", "--workspaces"], + name: ['-ws', '--workspaces'], description: - "Enable running a command in the context of all the configured workspaces", + 'Enable running a command in the context of all the configured workspaces', }, ]; const npmUninstallOptions: Fig.Option[] = [ { - name: ["-S", "--save"], - description: "Package will be removed from your dependencies", + name: ['-S', '--save'], + description: 'Package will be removed from your dependencies', }, { - name: ["-D", "--save-dev"], - description: "Package will appear in your `devDependencies`", + name: ['-D', '--save-dev'], + description: 'Package will appear in your `devDependencies`', }, { - name: ["-O", "--save-optional"], - description: "Package will appear in your `optionalDependencies`", + name: ['-O', '--save-optional'], + description: 'Package will appear in your `optionalDependencies`', }, { - name: "--no-save", - description: "Prevents saving to `dependencies`", + name: '--no-save', + description: 'Prevents saving to `dependencies`', }, { - name: "-g", - description: "Uninstall global package", + name: '-g', + description: 'Uninstall global package', }, ...workSpaceOptions, ]; const npmListOptions: Fig.Option[] = [ { - name: ["-a", "-all"], - description: "Show all outdated or installed packages", + name: ['-a', '-all'], + description: 'Show all outdated or installed packages', }, jsonOption, longOption, parseableOption, { - name: "--depth", - description: "The depth to go when recursing packages", - args: { name: "depth" }, + name: '--depth', + description: 'The depth to go when recursing packages', + args: { name: 'depth' }, }, { - name: "--link", - description: "Limits output to only those packages that are linked", + name: '--link', + description: 'Limits output to only those packages that are linked', }, { - name: "--package-lock-only", + name: '--package-lock-only', description: - "Current operation will only use the package-lock.json, ignoring node_modules", + 'Current operation will only use the package-lock.json, ignoring node_modules', }, { - name: "--no-unicode", - description: "Uses unicode characters in the tree output", + name: '--no-unicode', + description: 'Uses unicode characters in the tree output', }, globalOption, omitOption, @@ -359,54 +360,54 @@ const npmListOptions: Fig.Option[] = [ ]; const registryOption: Fig.Option = { - name: "--registry", - description: "The base URL of the npm registry", - args: { name: "registry" }, + name: '--registry', + description: 'The base URL of the npm registry', + args: { name: 'registry' }, }; const verboseOption: Fig.Option = { - name: "--verbose", - description: "Show extra information", - args: { name: "verbose" }, + name: '--verbose', + description: 'Show extra information', + args: { name: 'verbose' }, }; const otpOption: Fig.Option = { - name: "--otp", - description: "One-time password from a two-factor authenticator", - args: { name: "otp" }, + name: '--otp', + description: 'One-time password from a two-factor authenticator', + args: { name: 'otp' }, }; const ignoreScriptsOption: Fig.Option = { - name: "--ignore-scripts", + name: '--ignore-scripts', description: - "If true, npm does not run scripts specified in package.json files", + 'If true, npm does not run scripts specified in package.json files', }; const scriptShellOption: Fig.Option = { - name: "--script-shell", + name: '--script-shell', description: - "The shell to use for scripts run with the npm exec, npm run and npm init commands", - args: { name: "script-shell" }, + 'The shell to use for scripts run with the npm exec, npm run and npm init commands', + args: { name: 'script-shell' }, }; const dryRunOption: Fig.Option = { - name: "--dry-run", + name: '--dry-run', description: - "Indicates that you don't want npm to make any changes and that it should only report what it would have done", + 'Indicates that you don\'t want npm to make any changes and that it should only report what it would have done', }; const completionSpec: Fig.Spec = { - name: "npm", + name: 'npm', parserDirectives: { flagsArePosixNoncompliant: true, }, - description: "Node package manager", + description: 'Node package manager', subcommands: [ { - name: ["install", "i", "add"], - description: "Install a package and its dependencies", + name: ['install', 'i', 'add'], + description: 'Install a package and its dependencies', args: { - name: "package", + name: 'package', isOptional: true, generators: npmSearchGenerator, debounce: true, @@ -414,160 +415,160 @@ const completionSpec: Fig.Spec = { }, options: [ { - name: ["-P", "--save-prod"], + name: ['-P', '--save-prod'], description: - "Package will appear in your `dependencies`. This is the default unless `-D` or `-O` are present", + 'Package will appear in your `dependencies`. This is the default unless `-D` or `-O` are present', }, { - name: ["-D", "--save-dev"], - description: "Package will appear in your `devDependencies`", + name: ['-D', '--save-dev'], + description: 'Package will appear in your `devDependencies`', }, { - name: ["-O", "--save-optional"], - description: "Package will appear in your `optionalDependencies`", + name: ['-O', '--save-optional'], + description: 'Package will appear in your `optionalDependencies`', }, { - name: "--no-save", - description: "Prevents saving to `dependencies`", + name: '--no-save', + description: 'Prevents saving to `dependencies`', }, { - name: ["-E", "--save-exact"], + name: ['-E', '--save-exact'], description: - "Saved dependencies will be configured with an exact version rather than using npm's default semver range operator", + 'Saved dependencies will be configured with an exact version rather than using npm\'s default semver range operator', }, { - name: ["-B", "--save-bundle"], + name: ['-B', '--save-bundle'], description: - "Saved dependencies will also be added to your bundleDependencies list", + 'Saved dependencies will also be added to your bundleDependencies list', }, globalOption, { - name: "--global-style", + name: '--global-style', description: - "Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder", + 'Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder', }, { - name: "--legacy-bundling", + name: '--legacy-bundling', description: - "Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package", + 'Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package', }, { - name: "--legacy-peer-deps", + name: '--legacy-peer-deps', description: - "Bypass peerDependency auto-installation. Emulate install behavior of NPM v4 through v6", + 'Bypass peerDependency auto-installation. Emulate install behavior of NPM v4 through v6', }, { - name: "--strict-peer-deps", + name: '--strict-peer-deps', description: - "If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure", + 'If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure', }, { - name: "--no-package-lock", - description: "Ignores package-lock.json files when installing", + name: '--no-package-lock', + description: 'Ignores package-lock.json files when installing', }, registryOption, verboseOption, omitOption, ignoreScriptsOption, { - name: "--no-audit", + name: '--no-audit', description: - "Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes", + 'Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', }, { - name: "--no-bin-links", + name: '--no-bin-links', description: - "Tells npm to not create symlinks (or .cmd shims on Windows) for package executables", + 'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables', }, { - name: "--no-fund", + name: '--no-fund', description: - "Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding", + 'Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding', }, dryRunOption, ...workSpaceOptions, ], }, { - name: ["run", "run-script"], - description: "Run arbitrary package scripts", + name: ['run', 'run-script'], + description: 'Run arbitrary package scripts', options: [ ...workSpaceOptions, { - name: "--if-present", + name: '--if-present', description: - "Npm will not exit with an error code when run-script is invoked for a script that isn't defined in the scripts section of package.json", + 'Npm will not exit with an error code when run-script is invoked for a script that isn\'t defined in the scripts section of package.json', }, { - name: "--silent", - description: "", + name: '--silent', + description: '', }, ignoreScriptsOption, scriptShellOption, { - name: "--", + name: '--', args: { - name: "args", + name: 'args', isVariadic: true, // TODO: load the spec based on the runned script (see yarn spec `yarnScriptParsedDirectives`) }, }, ], args: { - name: "script", - description: "Script to run from your package.json", - filterStrategy: "fuzzy", + name: 'script', + description: 'Script to run from your package.json', + filterStrategy: 'fuzzy', generators: npmScriptsGenerator, }, }, { - name: "init", - description: "Trigger the initialization", + name: 'init', + description: 'Trigger the initialization', options: [ { - name: ["-y", "--yes"], + name: ['-y', '--yes'], description: - "Automatically answer 'yes' to any prompts that npm might print on the command line", + 'Automatically answer \'yes\' to any prompts that npm might print on the command line', }, { - name: "-w", + name: '-w', description: - "Create the folders and boilerplate expected while also adding a reference to your project workspaces property", - args: { name: "dir" }, + 'Create the folders and boilerplate expected while also adding a reference to your project workspaces property', + args: { name: 'dir' }, }, ], }, - { name: "access", description: "Set access controls on private packages" }, + { name: 'access', description: 'Set access controls on private packages' }, { - name: ["adduser", "login"], - description: "Add a registry user account", + name: ['adduser', 'login'], + description: 'Add a registry user account', options: [ registryOption, { - name: "--scope", + name: '--scope', description: - "Associate an operation with a scope for a scoped registry", + 'Associate an operation with a scope for a scoped registry', args: { - name: "scope", - description: "Scope name", + name: 'scope', + description: 'Scope name', }, }, ], }, { - name: "audit", - description: "Run a security audit", + name: 'audit', + description: 'Run a security audit', subcommands: [ { - name: "fix", + name: 'fix', description: - "If the fix argument is provided, then remediations will be applied to the package tree", + 'If the fix argument is provided, then remediations will be applied to the package tree', options: [ dryRunOption, { - name: ["-f", "--force"], + name: ['-f', '--force'], description: - "Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input", + 'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input', isDangerous: true, }, ...workSpaceOptions, @@ -577,40 +578,40 @@ const completionSpec: Fig.Spec = { options: [ ...workSpaceOptions, { - name: "--audit-level", + name: '--audit-level', description: - "The minimum level of vulnerability for npm audit to exit with a non-zero exit code", + 'The minimum level of vulnerability for npm audit to exit with a non-zero exit code', args: { - name: "audit", + name: 'audit', suggestions: [ - "info", - "low", - "moderate", - "high", - "critical", - "none", + 'info', + 'low', + 'moderate', + 'high', + 'critical', + 'none', ], }, }, { - name: "--package-lock-only", + name: '--package-lock-only', description: - "Current operation will only use the package-lock.json, ignoring node_modules", + 'Current operation will only use the package-lock.json, ignoring node_modules', }, jsonOption, omitOption, ], }, { - name: "bin", - description: "Print the folder where npm will install executables", + name: 'bin', + description: 'Print the folder where npm will install executables', options: [globalOption], }, { - name: ["bugs", "issues"], - description: "Report bugs for a package in a web browser", + name: ['bugs', 'issues'], + description: 'Report bugs for a package in a web browser', args: { - name: "package", + name: 'package', isOptional: true, generators: npmSearchGenerator, debounce: true, @@ -618,65 +619,65 @@ const completionSpec: Fig.Spec = { }, options: [ { - name: "--no-browser", - description: "Display in command line instead of browser", - exclusiveOn: ["--browser"], + name: '--no-browser', + description: 'Display in command line instead of browser', + exclusiveOn: ['--browser'], }, { - name: "--browser", + name: '--browser', description: - "The browser that is called by the npm bugs command to open websites", - args: { name: "browser" }, - exclusiveOn: ["--no-browser"], + 'The browser that is called by the npm bugs command to open websites', + args: { name: 'browser' }, + exclusiveOn: ['--no-browser'], }, registryOption, ], }, { - name: "cache", - description: "Manipulates packages cache", + name: 'cache', + description: 'Manipulates packages cache', subcommands: [ { - name: "add", - description: "Add the specified packages to the local cache", + name: 'add', + description: 'Add the specified packages to the local cache', }, { - name: "clean", - description: "Delete all data out of the cache folder", + name: 'clean', + description: 'Delete all data out of the cache folder', }, { - name: "verify", + name: 'verify', description: - "Verify the contents of the cache folder, garbage collecting any unneeded data, and verifying the integrity of the cache index and all cached data", + 'Verify the contents of the cache folder, garbage collecting any unneeded data, and verifying the integrity of the cache index and all cached data', }, ], options: [ { - name: "--cache", - args: { name: "cache" }, - description: "The location of npm's cache directory", + name: '--cache', + args: { name: 'cache' }, + description: 'The location of npm\'s cache directory', }, ], }, { - name: ["ci", "clean-install", "install-clean"], - description: "Install a project with a clean slate", + name: ['ci', 'clean-install', 'install-clean'], + description: 'Install a project with a clean slate', options: [ { - name: "--audit", + name: '--audit', description: - 'When "true" submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', + 'When \'true\' submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', args: { - name: "audit", - suggestions: ["true", "false"], + name: 'audit', + suggestions: ['true', 'false'], }, - exclusiveOn: ["--no-audit"], + exclusiveOn: ['--no-audit'], }, { - name: "--no-audit", + name: '--no-audit', description: - "Do not submit audit reports alongside the current npm command", - exclusiveOn: ["--audit"], + 'Do not submit audit reports alongside the current npm command', + exclusiveOn: ['--audit'], }, ignoreScriptsOption, scriptShellOption, @@ -685,70 +686,70 @@ const completionSpec: Fig.Spec = { ], }, { - name: "cit", - description: "Install a project with a clean slate and run tests", + name: 'cit', + description: 'Install a project with a clean slate and run tests', }, { - name: "clean-install-test", - description: "Install a project with a clean slate and run tests", + name: 'clean-install-test', + description: 'Install a project with a clean slate and run tests', }, - { name: "completion", description: "Tab completion for npm" }, + { name: 'completion', description: 'Tab completion for npm' }, { - name: ["config", "c"], - description: "Manage the npm configuration files", + name: ['config', 'c'], + description: 'Manage the npm configuration files', subcommands: [ { - name: "set", - description: "Sets the config key to the value", - args: [{ name: "key" }, { name: "value" }], + name: 'set', + description: 'Sets the config key to the value', + args: [{ name: 'key' }, { name: 'value' }], options: [ - { name: ["-g", "--global"], description: "Sets it globally" }, + { name: ['-g', '--global'], description: 'Sets it globally' }, ], }, { - name: "get", - description: "Echo the config value to stdout", - args: { name: "key" }, + name: 'get', + description: 'Echo the config value to stdout', + args: { name: 'key' }, }, { - name: "list", - description: "Show all the config settings", + name: 'list', + description: 'Show all the config settings', options: [ - { name: "-g", description: "Lists globally installed packages" }, - { name: "-l", description: "Also shows defaults" }, + { name: '-g', description: 'Lists globally installed packages' }, + { name: '-l', description: 'Also shows defaults' }, jsonOption, ], }, { - name: "delete", - description: "Deletes the key from all configuration files", - args: { name: "key" }, + name: 'delete', + description: 'Deletes the key from all configuration files', + args: { name: 'key' }, }, { - name: "edit", - description: "Opens the config file in an editor", + name: 'edit', + description: 'Opens the config file in an editor', options: [ - { name: "--global", description: "Edits the global config" }, + { name: '--global', description: 'Edits the global config' }, ], }, ], }, - { name: "create", description: "Create a package.json file" }, + { name: 'create', description: 'Create a package.json file' }, { - name: ["dedupe", "ddp"], - description: "Reduce duplication in the package tree", + name: ['dedupe', 'ddp'], + description: 'Reduce duplication in the package tree', }, { - name: "deprecate", - description: "Deprecate a version of a package", + name: 'deprecate', + description: 'Deprecate a version of a package', options: [registryOption], }, - { name: "dist-tag", description: "Modify package distribution tags" }, + { name: 'dist-tag', description: 'Modify package distribution tags' }, { - name: ["docs", "home"], - description: "Open documentation for a package in a web browser", + name: ['docs', 'home'], + description: 'Open documentation for a package in a web browser', args: { - name: "package", + name: 'package', isOptional: true, generators: npmSearchGenerator, debounce: true, @@ -758,158 +759,158 @@ const completionSpec: Fig.Spec = { ...workSpaceOptions, registryOption, { - name: "--no-browser", - description: "Display in command line instead of browser", - exclusiveOn: ["--browser"], + name: '--no-browser', + description: 'Display in command line instead of browser', + exclusiveOn: ['--browser'], }, { - name: "--browser", + name: '--browser', description: - "The browser that is called by the npm docs command to open websites", - args: { name: "browser" }, - exclusiveOn: ["--no-browser"], + 'The browser that is called by the npm docs command to open websites', + args: { name: 'browser' }, + exclusiveOn: ['--no-browser'], }, ], }, { - name: "doctor", - description: "Check your npm environment", + name: 'doctor', + description: 'Check your npm environment', options: [registryOption], }, { - name: "edit", - description: "Edit an installed package", + name: 'edit', + description: 'Edit an installed package', options: [ { - name: "--editor", - description: "The command to run for npm edit or npm config edit", + name: '--editor', + description: 'The command to run for npm edit or npm config edit', }, ], }, { - name: "explore", - description: "Browse an installed package", + name: 'explore', + description: 'Browse an installed package', args: { - name: "package", - filterStrategy: "fuzzy", + name: 'package', + filterStrategy: 'fuzzy', generators: dependenciesGenerator, }, }, - { name: "fund", description: "Retrieve funding information" }, - { name: "get", description: "Echo the config value to stdout" }, + { name: 'fund', description: 'Retrieve funding information' }, + { name: 'get', description: 'Echo the config value to stdout' }, { - name: "help", - description: "Get help on npm", + name: 'help', + description: 'Get help on npm', args: { - name: "term", + name: 'term', isVariadic: true, - description: "Terms to search for", + description: 'Terms to search for', }, options: [ { - name: "--viewer", - description: "The program to use to view help content", + name: '--viewer', + description: 'The program to use to view help content', args: { - name: "viewer", + name: 'viewer', }, }, ], }, { - name: "help-search", - description: "Search npm help documentation", + name: 'help-search', + description: 'Search npm help documentation', args: { - name: "text", - description: "Text to search for", + name: 'text', + description: 'Text to search for', }, options: [longOption], }, - { name: "hook", description: "Manage registry hooks" }, + { name: 'hook', description: 'Manage registry hooks' }, { - name: "install-ci-test", - description: "Install a project with a clean slate and run tests", + name: 'install-ci-test', + description: 'Install a project with a clean slate and run tests', }, - { name: "install-test", description: "Install package(s) and run tests" }, - { name: "it", description: "Install package(s) and run tests" }, + { name: 'install-test', description: 'Install package(s) and run tests' }, + { name: 'it', description: 'Install package(s) and run tests' }, { - name: "link", - description: "Symlink a package folder", - args: { name: "path", template: "filepaths" }, + name: 'link', + description: 'Symlink a package folder', + args: { name: 'path', template: 'filepaths' }, }, - { name: "ln", description: "Symlink a package folder" }, + { name: 'ln', description: 'Symlink a package folder' }, { - name: "logout", - description: "Log out of the registry", + name: 'logout', + description: 'Log out of the registry', options: [ registryOption, { - name: "--scope", + name: '--scope', description: - "Associate an operation with a scope for a scoped registry", + 'Associate an operation with a scope for a scoped registry', args: { - name: "scope", - description: "Scope name", + name: 'scope', + description: 'Scope name', }, }, ], }, { - name: ["ls", "list"], - description: "List installed packages", + name: ['ls', 'list'], + description: 'List installed packages', options: npmListOptions, - args: { name: "[@scope]/pkg", isVariadic: true }, + args: { name: '[@scope]/pkg', isVariadic: true }, }, { - name: "org", - description: "Manage orgs", + name: 'org', + description: 'Manage orgs', subcommands: [ { - name: "set", - description: "Add a user to an org or manage roles", + name: 'set', + description: 'Add a user to an org or manage roles', args: [ { - name: "orgname", - description: "Organization name", + name: 'orgname', + description: 'Organization name', }, { - name: "username", - description: "User name", + name: 'username', + description: 'User name', }, { - name: "role", + name: 'role', isOptional: true, - suggestions: ["developer", "admin", "owner"], + suggestions: ['developer', 'admin', 'owner'], }, ], options: [registryOption, otpOption], }, { - name: "rm", - description: "Remove a user from an org", + name: 'rm', + description: 'Remove a user from an org', args: [ { - name: "orgname", - description: "Organization name", + name: 'orgname', + description: 'Organization name', }, { - name: "username", - description: "User name", + name: 'username', + description: 'User name', }, ], options: [registryOption, otpOption], }, { - name: "ls", + name: 'ls', description: - "List users in an org or see what roles a particular user has in an org", + 'List users in an org or see what roles a particular user has in an org', args: [ { - name: "orgname", - description: "Organization name", + name: 'orgname', + description: 'Organization name', }, { - name: "username", - description: "User name", + name: 'username', + description: 'User name', isOptional: true, }, ], @@ -918,133 +919,133 @@ const completionSpec: Fig.Spec = { ], }, { - name: "outdated", - description: "Check for outdated packages", + name: 'outdated', + description: 'Check for outdated packages', args: { - name: "[<@scope>/]", + name: '[<@scope>/]', isVariadic: true, isOptional: true, }, options: [ { - name: ["-a", "-all"], - description: "Show all outdated or installed packages", + name: ['-a', '-all'], + description: 'Show all outdated or installed packages', }, jsonOption, longOption, parseableOption, { - name: "-g", - description: "Checks globally", + name: '-g', + description: 'Checks globally', }, ...workSpaceOptions, ], }, { - name: ["owner", "author"], - description: "Manage package owners", + name: ['owner', 'author'], + description: 'Manage package owners', subcommands: [ { - name: "ls", + name: 'ls', description: - "List all the users who have access to modify a package and push new versions. Handy when you need to know who to bug for help", - args: { name: "[@scope/]pkg" }, + 'List all the users who have access to modify a package and push new versions. Handy when you need to know who to bug for help', + args: { name: '[@scope/]pkg' }, options: [registryOption], }, { - name: "add", + name: 'add', description: - "Add a new user as a maintainer of a package. This user is enabled to modify metadata, publish new versions, and add other owners", - args: [{ name: "user" }, { name: "[@scope/]pkg" }], + 'Add a new user as a maintainer of a package. This user is enabled to modify metadata, publish new versions, and add other owners', + args: [{ name: 'user' }, { name: '[@scope/]pkg' }], options: [registryOption, otpOption], }, { - name: "rm", + name: 'rm', description: - "Remove a user from the package owner list. This immediately revokes their privileges", - args: [{ name: "user" }, { name: "[@scope/]pkg" }], + 'Remove a user from the package owner list. This immediately revokes their privileges', + args: [{ name: 'user' }, { name: '[@scope/]pkg' }], options: [registryOption, otpOption], }, ], }, { - name: "pack", - description: "Create a tarball from a package", + name: 'pack', + description: 'Create a tarball from a package', args: { - name: "[<@scope>/]", + name: '[<@scope>/]', }, options: [ jsonOption, dryRunOption, ...workSpaceOptions, { - name: "--pack-destination", - description: "Directory in which npm pack will save tarballs", + name: '--pack-destination', + description: 'Directory in which npm pack will save tarballs', args: { - name: "pack-destination", - template: ["folders"], + name: 'pack-destination', + template: ['folders'], }, }, ], }, { - name: "ping", - description: "Ping npm registry", + name: 'ping', + description: 'Ping npm registry', options: [registryOption], }, { - name: "pkg", - description: "Manages your package.json", + name: 'pkg', + description: 'Manages your package.json', subcommands: [ { - name: "get", + name: 'get', description: - "Retrieves a value key, defined in your package.json file. It is possible to get multiple values and values for child fields", + 'Retrieves a value key, defined in your package.json file. It is possible to get multiple values and values for child fields', args: { - name: "field", + name: 'field', description: - "Name of the field to get. You can view child fields by separating them with a period", + 'Name of the field to get. You can view child fields by separating them with a period', isVariadic: true, }, options: [jsonOption, ...workSpaceOptions], }, { - name: "set", + name: 'set', description: - "Sets a value in your package.json based on the field value. It is possible to set multiple values and values for child fields", + 'Sets a value in your package.json based on the field value. It is possible to set multiple values and values for child fields', args: { // Format is =. How to achieve this? - name: "field", + name: 'field', description: - "Name of the field to set. You can set child fields by separating them with a period", + 'Name of the field to set. You can set child fields by separating them with a period', isVariadic: true, }, options: [ jsonOption, ...workSpaceOptions, { - name: ["-f", "--force"], + name: ['-f', '--force'], description: - "Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg", + 'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg', isDangerous: true, }, ], }, { - name: "delete", - description: "Deletes a key from your package.json", + name: 'delete', + description: 'Deletes a key from your package.json', args: { - name: "key", + name: 'key', description: - "Name of the key to delete. You can delete child fields by separating them with a period", + 'Name of the key to delete. You can delete child fields by separating them with a period', isVariadic: true, }, options: [ ...workSpaceOptions, { - name: ["-f", "--force"], + name: ['-f', '--force'], description: - "Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg", + 'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg', isDangerous: true, }, ], @@ -1052,95 +1053,95 @@ const completionSpec: Fig.Spec = { ], }, { - name: "prefix", - description: "Display prefix", + name: 'prefix', + description: 'Display prefix', options: [ { - name: ["-g", "--global"], - description: "Print the global prefix to standard out", + name: ['-g', '--global'], + description: 'Print the global prefix to standard out', }, ], }, { - name: "profile", - description: "Change settings on your registry profile", + name: 'profile', + description: 'Change settings on your registry profile', subcommands: [ { - name: "get", + name: 'get', description: - "Display all of the properties of your profile, or one or more specific properties", + 'Display all of the properties of your profile, or one or more specific properties', args: { - name: "property", + name: 'property', isOptional: true, - description: "Property name", + description: 'Property name', }, options: [registryOption, jsonOption, parseableOption, otpOption], }, { - name: "set", - description: "Set the value of a profile property", + name: 'set', + description: 'Set the value of a profile property', args: [ { - name: "property", - description: "Property name", + name: 'property', + description: 'Property name', suggestions: [ - "email", - "fullname", - "homepage", - "freenode", - "twitter", - "github", + 'email', + 'fullname', + 'homepage', + 'freenode', + 'twitter', + 'github', ], }, { - name: "value", - description: "Property value", + name: 'value', + description: 'Property value', }, ], options: [registryOption, jsonOption, parseableOption, otpOption], subcommands: [ { - name: "password", + name: 'password', description: - "Change your password. This is interactive, you'll be prompted for your current password and a new password", + 'Change your password. This is interactive, you\'ll be prompted for your current password and a new password', }, ], }, { - name: "enable-2fa", - description: "Enables two-factor authentication", + name: 'enable-2fa', + description: 'Enables two-factor authentication', args: { - name: "mode", + name: 'mode', description: - "Mode for two-factor authentication. Defaults to auth-and-writes mode", + 'Mode for two-factor authentication. Defaults to auth-and-writes mode', isOptional: true, suggestions: [ { - name: "auth-only", + name: 'auth-only', description: - "Require an OTP when logging in or making changes to your account's authentication", + 'Require an OTP when logging in or making changes to your account\'s authentication', }, { - name: "auth-and-writes", + name: 'auth-and-writes', description: - "Requires an OTP at all the times auth-only does, and also requires one when publishing a module, setting the latest dist-tag, or changing access via npm access and npm owner", + 'Requires an OTP at all the times auth-only does, and also requires one when publishing a module, setting the latest dist-tag, or changing access via npm access and npm owner', }, ], }, options: [registryOption, otpOption], }, { - name: "disable-2fa", - description: "Disables two-factor authentication", + name: 'disable-2fa', + description: 'Disables two-factor authentication', options: [registryOption, otpOption], }, ], }, { - name: "prune", - description: "Remove extraneous packages", + name: 'prune', + description: 'Remove extraneous packages', args: { - name: "[<@scope>/]", + name: '[<@scope>/]', isOptional: true, }, options: [ @@ -1148,36 +1149,36 @@ const completionSpec: Fig.Spec = { dryRunOption, jsonOption, { - name: "--production", - description: "Remove the packages specified in your devDependencies", + name: '--production', + description: 'Remove the packages specified in your devDependencies', }, ...workSpaceOptions, ], }, { - name: "publish", - description: "Publish a package", + name: 'publish', + description: 'Publish a package', args: { - name: "tarball|folder", + name: 'tarball|folder', isOptional: true, description: - "A url or file path to a gzipped tar archive containing a single folder with a package.json file inside | A folder containing a package.json file", - template: ["folders"], + 'A url or file path to a gzipped tar archive containing a single folder with a package.json file inside | A folder containing a package.json file', + template: ['folders'], }, options: [ { - name: "--tag", - description: "Registers the published package with the given tag", - args: { name: "tag" }, + name: '--tag', + description: 'Registers the published package with the given tag', + args: { name: 'tag' }, }, ...workSpaceOptions, { - name: "--access", + name: '--access', description: - "Sets scoped package to be publicly viewable if set to 'public'", + 'Sets scoped package to be publicly viewable if set to \'public\'', args: { - default: "restricted", - suggestions: ["restricted", "public"], + default: 'restricted', + suggestions: ['restricted', 'public'], }, }, dryRunOption, @@ -1185,27 +1186,27 @@ const completionSpec: Fig.Spec = { ], }, { - name: ["rebuild", "rb"], - description: "Rebuild a package", + name: ['rebuild', 'rb'], + description: 'Rebuild a package', args: { - name: "[<@scope>/][@]", + name: '[<@scope>/][@]', }, options: [ globalOption, ...workSpaceOptions, ignoreScriptsOption, { - name: "--no-bin-links", + name: '--no-bin-links', description: - "Tells npm to not create symlinks (or .cmd shims on Windows) for package executables", + 'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables', }, ], }, { - name: "repo", - description: "Open package repository page in the browser", + name: 'repo', + description: 'Open package repository page in the browser', args: { - name: "package", + name: 'package', isOptional: true, generators: npmSearchGenerator, debounce: true, @@ -1214,394 +1215,394 @@ const completionSpec: Fig.Spec = { options: [ ...workSpaceOptions, { - name: "--no-browser", - description: "Display in command line instead of browser", - exclusiveOn: ["--browser"], + name: '--no-browser', + description: 'Display in command line instead of browser', + exclusiveOn: ['--browser'], }, { - name: "--browser", + name: '--browser', description: - "The browser that is called by the npm repo command to open websites", - args: { name: "browser" }, - exclusiveOn: ["--no-browser"], + 'The browser that is called by the npm repo command to open websites', + args: { name: 'browser' }, + exclusiveOn: ['--no-browser'], }, ], }, { - name: "restart", - description: "Restart a package", + name: 'restart', + description: 'Restart a package', options: [ ignoreScriptsOption, scriptShellOption, { - name: "--", + name: '--', args: { - name: "arg", - description: "Arguments to be passed to the restart script", + name: 'arg', + description: 'Arguments to be passed to the restart script', }, }, ], }, { - name: "root", - description: "Display npm root", + name: 'root', + description: 'Display npm root', options: [ { - name: ["-g", "--global"], + name: ['-g', '--global'], description: - "Print the effective global node_modules folder to standard out", + 'Print the effective global node_modules folder to standard out', }, ], }, { - name: ["search", "s", "se", "find"], - description: "Search for packages", + name: ['search', 's', 'se', 'find'], + description: 'Search for packages', args: { - name: "search terms", + name: 'search terms', isVariadic: true, }, options: [ longOption, jsonOption, { - name: "--color", - description: "Show colors", + name: '--color', + description: 'Show colors', args: { - name: "always", - suggestions: ["always"], - description: "Always show colors", + name: 'always', + suggestions: ['always'], + description: 'Always show colors', }, - exclusiveOn: ["--no-color"], + exclusiveOn: ['--no-color'], }, { - name: "--no-color", - description: "Do not show colors", - exclusiveOn: ["--color"], + name: '--no-color', + description: 'Do not show colors', + exclusiveOn: ['--color'], }, parseableOption, { - name: "--no-description", - description: "Do not show descriptions", + name: '--no-description', + description: 'Do not show descriptions', }, { - name: "--searchopts", + name: '--searchopts', description: - "Space-separated options that are always passed to search", + 'Space-separated options that are always passed to search', args: { - name: "searchopts", + name: 'searchopts', }, }, { - name: "--searchexclude", + name: '--searchexclude', description: - "Space-separated options that limit the results from search", + 'Space-separated options that limit the results from search', args: { - name: "searchexclude", + name: 'searchexclude', }, }, registryOption, { - name: "--prefer-online", + name: '--prefer-online', description: - "If true, staleness checks for cached data will be forced, making the CLI look for updates immediately even for fresh package data", - exclusiveOn: ["--prefer-offline", "--offline"], + 'If true, staleness checks for cached data will be forced, making the CLI look for updates immediately even for fresh package data', + exclusiveOn: ['--prefer-offline', '--offline'], }, { - name: "--prefer-offline", + name: '--prefer-offline', description: - "If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server", - exclusiveOn: ["--prefer-online", "--offline"], + 'If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server', + exclusiveOn: ['--prefer-online', '--offline'], }, { - name: "--offline", + name: '--offline', description: - "Force offline mode: no network requests will be done during install", - exclusiveOn: ["--prefer-online", "--prefer-offline"], + 'Force offline mode: no network requests will be done during install', + exclusiveOn: ['--prefer-online', '--prefer-offline'], }, ], }, - { name: "set", description: "Sets the config key to the value" }, + { name: 'set', description: 'Sets the config key to the value' }, { - name: "set-script", - description: "Set tasks in the scripts section of package.json", + name: 'set-script', + description: 'Set tasks in the scripts section of package.json', args: [ { - name: "script", + name: 'script', description: - "Name of the task to be added to the scripts section of package.json", + 'Name of the task to be added to the scripts section of package.json', }, { - name: "command", - description: "Command to run when script is called", + name: 'command', + description: 'Command to run when script is called', }, ], options: workSpaceOptions, }, { - name: "shrinkwrap", - description: "Lock down dependency versions for publication", + name: 'shrinkwrap', + description: 'Lock down dependency versions for publication', }, { - name: "star", - description: "Mark your favorite packages", + name: 'star', + description: 'Mark your favorite packages', args: { - name: "pkg", - description: "Package to mark as favorite", + name: 'pkg', + description: 'Package to mark as favorite', }, options: [ registryOption, { - name: "--no-unicode", - description: "Do not use unicode characters in the tree output", + name: '--no-unicode', + description: 'Do not use unicode characters in the tree output', }, ], }, { - name: "stars", - description: "View packages marked as favorites", + name: 'stars', + description: 'View packages marked as favorites', args: { - name: "user", + name: 'user', isOptional: true, - description: "View packages marked as favorites by ", + description: 'View packages marked as favorites by ', }, options: [registryOption], }, { - name: "start", - description: "Start a package", + name: 'start', + description: 'Start a package', options: [ ignoreScriptsOption, scriptShellOption, { - name: "--", + name: '--', args: { - name: "arg", - description: "Arguments to be passed to the start script", + name: 'arg', + description: 'Arguments to be passed to the start script', }, }, ], }, { - name: "stop", - description: "Stop a package", + name: 'stop', + description: 'Stop a package', options: [ ignoreScriptsOption, scriptShellOption, { - name: "--", + name: '--', args: { - name: "arg", - description: "Arguments to be passed to the stop script", + name: 'arg', + description: 'Arguments to be passed to the stop script', }, }, ], }, { - name: "team", - description: "Manage organization teams and team memberships", + name: 'team', + description: 'Manage organization teams and team memberships', subcommands: [ { - name: "create", - args: { name: "scope:team" }, + name: 'create', + args: { name: 'scope:team' }, options: [registryOption, otpOption], }, { - name: "destroy", - args: { name: "scope:team" }, + name: 'destroy', + args: { name: 'scope:team' }, options: [registryOption, otpOption], }, { - name: "add", - args: [{ name: "scope:team" }, { name: "user" }], + name: 'add', + args: [{ name: 'scope:team' }, { name: 'user' }], options: [registryOption, otpOption], }, { - name: "rm", - args: [{ name: "scope:team" }, { name: "user" }], + name: 'rm', + args: [{ name: 'scope:team' }, { name: 'user' }], options: [registryOption, otpOption], }, { - name: "ls", - args: { name: "scope|scope:team" }, + name: 'ls', + args: { name: 'scope|scope:team' }, options: [registryOption, jsonOption, parseableOption], }, ], }, { - name: ["test", "tst", "t"], - description: "Test a package", + name: ['test', 'tst', 't'], + description: 'Test a package', options: [ignoreScriptsOption, scriptShellOption], }, { - name: "token", - description: "Manage your authentication tokens", + name: 'token', + description: 'Manage your authentication tokens', subcommands: [ { - name: "list", - description: "Shows a table of all active authentication tokens", + name: 'list', + description: 'Shows a table of all active authentication tokens', options: [jsonOption, parseableOption], }, { - name: "create", - description: "Create a new authentication token", + name: 'create', + description: 'Create a new authentication token', options: [ { - name: "--read-only", + name: '--read-only', description: - "This is used to mark a token as unable to publish when configuring limited access tokens with the npm token create command", + 'This is used to mark a token as unable to publish when configuring limited access tokens with the npm token create command', }, { - name: "--cidr", + name: '--cidr', description: - "This is a list of CIDR address to be used when configuring limited access tokens with the npm token create command", + 'This is a list of CIDR address to be used when configuring limited access tokens with the npm token create command', isRepeatable: true, args: { - name: "cidr", + name: 'cidr', }, }, ], }, { - name: "revoke", + name: 'revoke', description: - "Immediately removes an authentication token from the registry. You will no longer be able to use it", - args: { name: "idtoken" }, + 'Immediately removes an authentication token from the registry. You will no longer be able to use it', + args: { name: 'idtoken' }, }, ], options: [registryOption, otpOption], }, - uninstallSubcommand("uninstall"), - uninstallSubcommand(["r", "rm"]), - uninstallSubcommand("un"), - uninstallSubcommand("remove"), - uninstallSubcommand("unlink"), + uninstallSubcommand('uninstall'), + uninstallSubcommand(['r', 'rm']), + uninstallSubcommand('un'), + uninstallSubcommand('remove'), + uninstallSubcommand('unlink'), { - name: "unpublish", - description: "Remove a package from the registry", + name: 'unpublish', + description: 'Remove a package from the registry', args: { - name: "[<@scope>/][@]", + name: '[<@scope>/][@]', }, options: [ dryRunOption, { - name: ["-f", "--force"], + name: ['-f', '--force'], description: - "Allow unpublishing all versions of a published package. Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input", + 'Allow unpublishing all versions of a published package. Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input', isDangerous: true, }, ...workSpaceOptions, ], }, { - name: "unstar", - description: "Remove an item from your favorite packages", + name: 'unstar', + description: 'Remove an item from your favorite packages', args: { - name: "pkg", - description: "Package to unmark as favorite", + name: 'pkg', + description: 'Package to unmark as favorite', }, options: [ registryOption, otpOption, { - name: "--no-unicode", - description: "Do not use unicode characters in the tree output", + name: '--no-unicode', + description: 'Do not use unicode characters in the tree output', }, ], }, { - name: ["update", "upgrade", "up"], - description: "Update a package", + name: ['update', 'upgrade', 'up'], + description: 'Update a package', options: [ - { name: "-g", description: "Update global package" }, + { name: '-g', description: 'Update global package' }, { - name: "--global-style", + name: '--global-style', description: - "Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder", + 'Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder', }, { - name: "--legacy-bundling", + name: '--legacy-bundling', description: - "Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package", + 'Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package', }, { - name: "--strict-peer-deps", + name: '--strict-peer-deps', description: - "If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure", + 'If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure', }, { - name: "--no-package-lock", - description: "Ignores package-lock.json files when installing", + name: '--no-package-lock', + description: 'Ignores package-lock.json files when installing', }, omitOption, ignoreScriptsOption, { - name: "--no-audit", + name: '--no-audit', description: - "Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes", + 'Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', }, { - name: "--no-bin-links", + name: '--no-bin-links', description: - "Tells npm to not create symlinks (or .cmd shims on Windows) for package executables", + 'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables', }, { - name: "--no-fund", + name: '--no-fund', description: - "Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding", + 'Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding', }, { - name: "--save", + name: '--save', description: - "Update the semver values of direct dependencies in your project package.json", + 'Update the semver values of direct dependencies in your project package.json', }, dryRunOption, ...workSpaceOptions, ], }, { - name: "version", - description: "Bump a package version", + name: 'version', + description: 'Bump a package version', options: [ ...workSpaceOptions, jsonOption, { - name: "--allow-same-version", + name: '--allow-same-version', description: - "Prevents throwing an error when npm version is used to set the new version to the same value as the current version", + 'Prevents throwing an error when npm version is used to set the new version to the same value as the current version', }, { - name: "--no-commit-hooks", + name: '--no-commit-hooks', description: - "Do not run git commit hooks when using the npm version command", + 'Do not run git commit hooks when using the npm version command', }, { - name: "--no-git-tag-version", + name: '--no-git-tag-version', description: - "Do not tag the commit when using the npm version command", + 'Do not tag the commit when using the npm version command', }, { - name: "--preid", + name: '--preid', description: - 'The "prerelease identifier" to use as a prefix for the "prerelease" part of a semver. Like the rc in 1.2.0-rc.8', + 'The \'prerelease identifier\' to use as a prefix for the \'prerelease\' part of a semver. Like the rc in 1.2.0-rc.8', args: { - name: "prerelease-id", + name: 'prerelease-id', }, }, { - name: "--sign-git-tag", + name: '--sign-git-tag', description: - "If set to true, then the npm version command will tag the version using -s to add a signature", + 'If set to true, then the npm version command will tag the version using -s to add a signature', }, ], }, { - name: ["view", "v", "info", "show"], - description: "View registry info", + name: ['view', 'v', 'info', 'show'], + description: 'View registry info', options: [...workSpaceOptions, jsonOption], }, { - name: "whoami", - description: "Display npm username", + name: 'whoami', + description: 'Display npm username', options: [registryOption], }, ], From a3a485373b9082e76dd41115ac7078be59237de0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:24:56 -0800 Subject: [PATCH 06/25] Remove emoji icons --- extensions/terminal-suggest/src/completions/npm.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/npm.ts b/extensions/terminal-suggest/src/completions/npm.ts index 111d502482c..5548ea66a3e 100644 --- a/extensions/terminal-suggest/src/completions/npm.ts +++ b/extensions/terminal-suggest/src/completions/npm.ts @@ -183,7 +183,6 @@ export const dependenciesGenerator: Fig.Generator = { }) .map((pkgName) => ({ name: pkgName, - icon: '📦', description: dependencies[pkgName] ? 'dependency' : optionalDependencies[pkgName] @@ -197,7 +196,6 @@ export const dependenciesGenerator: Fig.Generator = { }); return stdout.split('\n').map((name) => ({ name, - icon: '📦', description: 'Global dependency', })); } From 7b6fa0cea4b30fad8ed9e18339db3b18b2995db3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:28:25 -0800 Subject: [PATCH 07/25] Add --package-lock-only to npm i flags Fixes #284593 --- extensions/terminal-suggest/src/completions/npm.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/terminal-suggest/src/completions/npm.ts b/extensions/terminal-suggest/src/completions/npm.ts index 5548ea66a3e..070f70de133 100644 --- a/extensions/terminal-suggest/src/completions/npm.ts +++ b/extensions/terminal-suggest/src/completions/npm.ts @@ -455,6 +455,10 @@ const completionSpec: Fig.Spec = { description: 'Bypass peerDependency auto-installation. Emulate install behavior of NPM v4 through v6', }, + { + name: '--package-lock-only', + description: 'Only update the `package-lock.json`, instead of checking `node_modules` and downloading dependencies.', + }, { name: '--strict-peer-deps', description: From def1b67d6871900861907e63776034fa102c71a5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:31:51 -0800 Subject: [PATCH 08/25] Move pnpm and yarn out of upstream --- .../src/completions/{upstream => }/pnpm.ts | 5 +++++ .../src/completions/{upstream => }/yarn.ts | 17 +++++++++++------ extensions/terminal-suggest/src/constants.ts | 2 -- .../terminal-suggest/src/terminalSuggestMain.ts | 4 ++++ 4 files changed, 20 insertions(+), 8 deletions(-) rename extensions/terminal-suggest/src/completions/{upstream => }/pnpm.ts (98%) rename extensions/terminal-suggest/src/completions/{upstream => }/yarn.ts (98%) diff --git a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts b/extensions/terminal-suggest/src/completions/pnpm.ts similarity index 98% rename from extensions/terminal-suggest/src/completions/upstream/pnpm.ts rename to extensions/terminal-suggest/src/completions/pnpm.ts index 9ce7c798208..ef4e67f0476 100644 --- a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts +++ b/extensions/terminal-suggest/src/completions/pnpm.ts @@ -1,3 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + // GENERATORS import { npmScriptsGenerator, npmSearchGenerator } from "./npm"; diff --git a/extensions/terminal-suggest/src/completions/upstream/yarn.ts b/extensions/terminal-suggest/src/completions/yarn.ts similarity index 98% rename from extensions/terminal-suggest/src/completions/upstream/yarn.ts rename to extensions/terminal-suggest/src/completions/yarn.ts index 04c573a151b..a0bbbcc0a8e 100644 --- a/extensions/terminal-suggest/src/completions/upstream/yarn.ts +++ b/extensions/terminal-suggest/src/completions/yarn.ts @@ -1,3 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { npmScriptsGenerator, npmSearchGenerator } from "./npm"; export const yarnScriptParserDirectives: Fig.Arg["parserDirectives"] = { @@ -82,7 +87,7 @@ const getGlobalPackagesGenerator: Fig.Generator = { name: dependencyName, icon: "📦", })); - } catch (e) {} + } catch (e) { } return []; }, @@ -101,7 +106,7 @@ const allDependenciesGenerator: Fig.Generator = { name: dependency.name.split("@")[0], icon: "📦", })); - } catch (e) {} + } catch (e) { } return []; }, }; @@ -127,7 +132,7 @@ const configList: Fig.Generator = { if (configObject) { return Object.keys(configObject).map((key) => ({ name: key })); } - } catch (e) {} + } catch (e) { } return []; }, @@ -1550,9 +1555,9 @@ const completionSpec: Fig.Spec = { try { const workspacesDefinitions = isYarnV1 ? // transform Yarn V1 output to array of workspaces like Yarn V2 - await getWorkspacesDefinitionsV1() + await getWorkspacesDefinitionsV1() : // in yarn v>=2.0.0, workspaces definitions are a list of JSON lines - await getWorkspacesDefinitionsVOther(); + await getWorkspacesDefinitionsVOther(); const subcommands: Fig.Subcommand[] = workspacesDefinitions.map( ({ name, location }: { name: string; location: string }) => ({ @@ -1578,7 +1583,7 @@ const completionSpec: Fig.Spec = { name: script, })); } - } catch (e) {} + } catch (e) { } return []; }, }, diff --git a/extensions/terminal-suggest/src/constants.ts b/extensions/terminal-suggest/src/constants.ts index 086d7ca8672..db376c2c3b4 100644 --- a/extensions/terminal-suggest/src/constants.ts +++ b/extensions/terminal-suggest/src/constants.ts @@ -114,8 +114,6 @@ export const upstreamSpecs = [ // JavaScript / TypeScript 'node', 'nvm', - 'pnpm', - 'yarn', 'yo', // Python diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 774a33f07da..95654ffe418 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -18,7 +18,9 @@ import gitCompletionSpec from './completions/git'; import ghCompletionSpec from './completions/gh'; import npmCompletionSpec from './completions/npm'; import npxCompletionSpec from './completions/npx'; +import pnpmCompletionSpec from './completions/pnpm'; import setLocationSpec from './completions/set-location'; +import yarnCompletionSpec from './completions/yarn'; import { upstreamSpecs } from './constants'; import { ITerminalEnvironment, PathExecutableCache } from './env/pathExecutableCache'; import { executeCommand, executeCommandTimeout, IFigExecuteExternals } from './fig/execute'; @@ -72,7 +74,9 @@ export const availableSpecs: Fig.Spec[] = [ ghCompletionSpec, npmCompletionSpec, npxCompletionSpec, + pnpmCompletionSpec, setLocationSpec, + yarnCompletionSpec, ]; for (const spec of upstreamSpecs) { availableSpecs.push(require(`./completions/upstream/${spec}`).default); From de7ad3fb3a71f2a73b84661bee8b5218c588585b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:33:05 -0800 Subject: [PATCH 09/25] Double to single quotes in pnpm --- .../terminal-suggest/src/completions/pnpm.ts | 562 +++++++++--------- 1 file changed, 281 insertions(+), 281 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/pnpm.ts b/extensions/terminal-suggest/src/completions/pnpm.ts index ef4e67f0476..55dade961d5 100644 --- a/extensions/terminal-suggest/src/completions/pnpm.ts +++ b/extensions/terminal-suggest/src/completions/pnpm.ts @@ -5,52 +5,52 @@ // GENERATORS -import { npmScriptsGenerator, npmSearchGenerator } from "./npm"; -import { dependenciesGenerator, nodeClis } from "./yarn"; +import { npmScriptsGenerator, npmSearchGenerator } from './npm'; +import { dependenciesGenerator, nodeClis } from './yarn'; const filterMessages = (out: string): string => { - return out.startsWith("warning:") || out.startsWith("error:") - ? out.split("\n").slice(1).join("\n") + return out.startsWith('warning:') || out.startsWith('error:') + ? out.split('\n').slice(1).join('\n') : out; }; const searchBranches: Fig.Generator = { - script: ["git", "branch", "--no-color"], + script: ['git', 'branch', '--no-color'], postProcess: function (out) { const output = filterMessages(out); - if (output.startsWith("fatal:")) { + if (output.startsWith('fatal:')) { return []; } - return output.split("\n").map((elm) => { + return output.split('\n').map((elm) => { let name = elm.trim(); const parts = elm.match(/\S+/g); if (parts && parts.length > 1) { - if (parts[0] == "*") { + if (parts[0] === '*') { // Current branch. return { - name: elm.replace("*", "").trim(), - description: "Current branch", - icon: "⭐️", + name: elm.replace('*', '').trim(), + description: 'Current branch', + icon: '⭐️', }; - } else if (parts[0] == "+") { + } else if (parts[0] === '+') { // Branch checked out in another worktree. - name = elm.replace("+", "").trim(); + name = elm.replace('+', '').trim(); } } return { name, - description: "Branch", - icon: "fig://icon?type=git", + description: 'Branch', + icon: 'fig://icon?type=git', }; }); }, }; const generatorInstalledPackages: Fig.Generator = { - script: ["pnpm", "ls"], + script: ['pnpm', 'ls'], postProcess: function (out) { /** * out @@ -68,38 +68,38 @@ const generatorInstalledPackages: Fig.Generator = { * typescript 4.7.4 * ``` */ - if (out.includes("ERR_PNPM")) { + if (out.includes('ERR_PNPM')) { return []; } const output = out - .split("\n") + .split('\n') .slice(3) - // remove empty lines, "*dependencies:" lines, local workspace packages (eg: "foo":"workspace:*") + // remove empty lines, '*dependencies:' lines, local workspace packages (eg: 'foo':'workspace:*') .filter( (item) => !!item && - !item.toLowerCase().includes("dependencies") && - !item.includes("link:") + !item.toLowerCase().includes('dependencies') && + !item.includes('link:') ) - .map((item) => item.replace(/\s/, "@")); // typescript 4.7.4 -> typescript@4.7.4 + .map((item) => item.replace(/\s/, '@')); // typescript 4.7.4 -> typescript@4.7.4 return output.map((pkg) => { return { name: pkg, - icon: "fig://icon?type=package", + icon: 'fig://icon?type=package', }; }); }, }; const FILTER_OPTION: Fig.Option = { - name: "--filter", + name: '--filter', args: { - template: "filepaths", - name: "Filepath / Package", + template: 'filepaths', + name: 'Filepath / Package', description: - "To only select packages under the specified directory, you may specify any absolute path, typically in POSIX format", + 'To only select packages under the specified directory, you may specify any absolute path, typically in POSIX format', }, description: `Filtering allows you to restrict commands to specific subsets of packages. pnpm supports a rich selector syntax for picking packages by name or by relation. @@ -109,26 +109,26 @@ More details: https://pnpm.io/filtering`, /** Options that being appended for `pnpm i` and `add` */ const INSTALL_BASE_OPTIONS: Fig.Option[] = [ { - name: "--offline", + name: '--offline', description: - "If true, pnpm will use only packages already available in the store. If a package won't be found locally, the installation will fail", + 'If true, pnpm will use only packages already available in the store. If a package won\'t be found locally, the installation will fail', }, { - name: "--prefer-offline", + name: '--prefer-offline', description: - "If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server. To force full offline mode, use --offline", + 'If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server. To force full offline mode, use --offline', }, { - name: "--ignore-scripts", + name: '--ignore-scripts', description: - "Do not execute any scripts defined in the project package.json and its dependencies", + 'Do not execute any scripts defined in the project package.json and its dependencies', }, { - name: "--reporter", + name: '--reporter', description: `Allows you to choose the reporter that will log debug info to the terminal about the installation progress`, args: { - name: "Reporter Type", - suggestions: ["silent", "default", "append-only", "ndjson"], + name: 'Reporter Type', + suggestions: ['silent', 'default', 'append-only', 'ndjson'], }, }, ]; @@ -136,80 +136,80 @@ const INSTALL_BASE_OPTIONS: Fig.Option[] = [ /** Base options for pnpm i when run without any arguments */ const INSTALL_OPTIONS: Fig.Option[] = [ { - name: ["-P", "--save-prod"], + name: ['-P', '--save-prod'], description: `Pnpm will not install any package listed in devDependencies if the NODE_ENV environment variable is set to production. Use this flag to instruct pnpm to ignore NODE_ENV and take its production status from this flag instead`, }, { - name: ["-D", "--save-dev"], + name: ['-D', '--save-dev'], description: - "Only devDependencies are installed regardless of the NODE_ENV", + 'Only devDependencies are installed regardless of the NODE_ENV', }, { - name: "--no-optional", - description: "OptionalDependencies are not installed", + name: '--no-optional', + description: 'OptionalDependencies are not installed', }, { - name: "--lockfile-only", + name: '--lockfile-only', description: - "When used, only updates pnpm-lock.yaml and package.json instead of checking node_modules and downloading dependencies", + 'When used, only updates pnpm-lock.yaml and package.json instead of checking node_modules and downloading dependencies', }, { - name: "--frozen-lockfile", + name: '--frozen-lockfile', description: - "If true, pnpm doesn't generate a lockfile and fails to install if the lockfile is out of sync with the manifest / an update is needed or no lockfile is present", + 'If true, pnpm doesn\'t generate a lockfile and fails to install if the lockfile is out of sync with the manifest / an update is needed or no lockfile is present', }, { - name: "--use-store-server", + name: '--use-store-server', description: - "Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run pnpm server stop", + 'Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run pnpm server stop', }, { - name: "--shamefully-hoist", + name: '--shamefully-hoist', description: - "Creates a flat node_modules structure, similar to that of npm or yarn. WARNING: This is highly discouraged", + 'Creates a flat node_modules structure, similar to that of npm or yarn. WARNING: This is highly discouraged', }, ]; /** Base options for pnpm add */ const INSTALL_PACKAGE_OPTIONS: Fig.Option[] = [ { - name: ["-P", "--save-prod"], - description: "Install the specified packages as regular dependencies", + name: ['-P', '--save-prod'], + description: 'Install the specified packages as regular dependencies', }, { - name: ["-D", "--save-dev"], - description: "Install the specified packages as devDependencies", + name: ['-D', '--save-dev'], + description: 'Install the specified packages as devDependencies', }, { - name: ["-O", "--save-optional"], - description: "Install the specified packages as optionalDependencies", + name: ['-O', '--save-optional'], + description: 'Install the specified packages as optionalDependencies', }, { - name: "--no-save", - description: "Prevents saving to `dependencies`", + name: '--no-save', + description: 'Prevents saving to `dependencies`', }, { - name: ["-E", "--save-exact"], + name: ['-E', '--save-exact'], description: - "Saved dependencies will be configured with an exact version rather than using pnpm's default semver range operator", + 'Saved dependencies will be configured with an exact version rather than using pnpm\'s default semver range operator', }, { - name: "--save-peer", + name: '--save-peer', description: - "Using --save-peer will add one or more packages to peerDependencies and install them as dev dependencies", + 'Using --save-peer will add one or more packages to peerDependencies and install them as dev dependencies', }, { - name: ["--ignore-workspace-root-check", "-W#"], + name: ['--ignore-workspace-root-check', '-W#'], description: `Adding a new dependency to the root workspace package fails, unless the --ignore-workspace-root-check or -W flag is used. For instance, pnpm add debug -W`, }, { - name: ["--global", "-g"], + name: ['--global', '-g'], description: `Install a package globally`, }, { - name: "--workspace", + name: '--workspace', description: `Only adds the new dependency if it is found in the workspace`, }, FILTER_OPTION, @@ -218,10 +218,10 @@ For instance, pnpm add debug -W`, // SUBCOMMANDS const SUBCOMMANDS_MANAGE_DEPENDENCIES: Fig.Subcommand[] = [ { - name: "add", + name: 'add', description: `Installs a package and any packages that it depends on. By default, any new package is installed as a production dependency`, args: { - name: "package", + name: 'package', generators: npmSearchGenerator, debounce: true, isVariadic: true, @@ -229,7 +229,7 @@ const SUBCOMMANDS_MANAGE_DEPENDENCIES: Fig.Subcommand[] = [ options: [...INSTALL_BASE_OPTIONS, ...INSTALL_PACKAGE_OPTIONS], }, { - name: ["install", "i"], + name: ['install', 'i'], description: `Pnpm install is used to install all dependencies for a project. In a CI environment, installation fails if a lockfile is present but needs an update. Inside a workspace, pnpm install installs all dependencies in all the projects. @@ -237,11 +237,11 @@ If you want to disable this behavior, set the recursive-install setting to false async generateSpec(tokens) { // `pnpm i` with args is an `pnpm add` alias const hasArgs = - tokens.filter((token) => token.trim() !== "" && !token.startsWith("-")) + tokens.filter((token) => token.trim() !== '' && !token.startsWith('-')) .length > 2; return { - name: "install", + name: 'install', options: [ ...INSTALL_BASE_OPTIONS, ...(hasArgs ? INSTALL_PACKAGE_OPTIONS : INSTALL_OPTIONS), @@ -249,7 +249,7 @@ If you want to disable this behavior, set the recursive-install setting to false }; }, args: { - name: "package", + name: 'package', isOptional: true, generators: npmSearchGenerator, debounce: true, @@ -257,56 +257,56 @@ If you want to disable this behavior, set the recursive-install setting to false }, }, { - name: ["install-test", "it"], + name: ['install-test', 'it'], description: - "Runs pnpm install followed immediately by pnpm test. It takes exactly the same arguments as pnpm install", + 'Runs pnpm install followed immediately by pnpm test. It takes exactly the same arguments as pnpm install', options: [...INSTALL_BASE_OPTIONS, ...INSTALL_OPTIONS], }, { - name: ["update", "upgrade", "up"], + name: ['update', 'upgrade', 'up'], description: `Pnpm update updates packages to their latest version based on the specified range. When used without arguments, updates all dependencies. You can use patterns to update specific dependencies`, args: { - name: "Package", + name: 'Package', isOptional: true, - filterStrategy: "fuzzy", + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, options: [ { - name: ["--recursive", "-r"], + name: ['--recursive', '-r'], description: - "Concurrently runs update in all subdirectories with a package.json (excluding node_modules)", + 'Concurrently runs update in all subdirectories with a package.json (excluding node_modules)', }, { - name: ["--latest", "-L"], + name: ['--latest', '-L'], description: - "Ignores the version range specified in package.json. Instead, the version specified by the latest tag will be used (potentially upgrading the packages across major versions)", + 'Ignores the version range specified in package.json. Instead, the version specified by the latest tag will be used (potentially upgrading the packages across major versions)', }, { - name: "--global", - description: "Update global packages", + name: '--global', + description: 'Update global packages', }, { - name: ["-P", "--save-prod"], + name: ['-P', '--save-prod'], description: `Only update packages in dependencies and optionalDependencies`, }, { - name: ["-D", "--save-dev"], - description: "Only update packages in devDependencies", + name: ['-D', '--save-dev'], + description: 'Only update packages in devDependencies', }, { - name: "--no-optional", - description: "Don't update packages in optionalDependencies", + name: '--no-optional', + description: 'Don\'t update packages in optionalDependencies', }, { - name: ["--interactive", "-i"], + name: ['--interactive', '-i'], description: - "Show outdated dependencies and select which ones to update", + 'Show outdated dependencies and select which ones to update', }, { - name: "--workspace", + name: '--workspace', description: `Tries to link all packages from the workspace. Versions are updated to match the versions of packages inside the workspace. If specific packages are updated, the command will fail if any of the updated dependencies are not found inside the workspace. For instance, the following command fails if express is not a workspace package: pnpm up -r --workspace express`, }, @@ -314,163 +314,163 @@ If specific packages are updated, the command will fail if any of the updated de ], }, { - name: ["remove", "rm", "uninstall", "un"], + name: ['remove', 'rm', 'uninstall', 'un'], description: `Removes packages from node_modules and from the project's package.json`, args: { - name: "Package", - filterStrategy: "fuzzy", + name: 'Package', + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, options: [ { - name: ["--recursive", "-r"], + name: ['--recursive', '-r'], description: `When used inside a workspace, removes a dependency (or dependencies) from every workspace package. When used not inside a workspace, removes a dependency (or dependencies) from every package found in subdirectories`, }, { - name: "--global", - description: "Remove a global package", + name: '--global', + description: 'Remove a global package', }, { - name: ["-P", "--save-prod"], + name: ['-P', '--save-prod'], description: `Only remove the dependency from dependencies`, }, { - name: ["-D", "--save-dev"], - description: "Only remove the dependency from devDependencies", + name: ['-D', '--save-dev'], + description: 'Only remove the dependency from devDependencies', }, { - name: ["--save-optional", "-O"], - description: "Only remove the dependency from optionalDependencies", + name: ['--save-optional', '-O'], + description: 'Only remove the dependency from optionalDependencies', }, FILTER_OPTION, ], }, { - name: ["link", "ln"], + name: ['link', 'ln'], description: `Makes the current local package accessible system-wide, or in another location`, args: [ { - name: "Package", - filterStrategy: "fuzzy", + name: 'Package', + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, - { template: "filepaths" }, + { template: 'filepaths' }, ], options: [ { - name: ["--dir", "-C"], + name: ['--dir', '-C'], description: `Changes the link location to `, }, { - name: "--global", + name: '--global', description: - "Links the specified package () from global node_modules to the node_nodules of package from where this command was executed or specified via --dir option", + 'Links the specified package () from global node_modules to the node_nodules of package from where this command was executed or specified via --dir option', }, ], }, { - name: "unlink", + name: 'unlink', description: `Unlinks a system-wide package (inverse of pnpm link). If called without arguments, all linked dependencies will be unlinked. This is similar to yarn unlink, except pnpm re-installs the dependency after removing the external link`, args: [ { - name: "Package", - filterStrategy: "fuzzy", + name: 'Package', + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, - { template: "filepaths" }, + { template: 'filepaths' }, ], options: [ { - name: ["--recursive", "-r"], + name: ['--recursive', '-r'], description: `Unlink in every package found in subdirectories or in every workspace package, when executed inside a workspace`, }, FILTER_OPTION, ], }, { - name: "import", + name: 'import', description: - "Pnpm import generates a pnpm-lock.yaml from an npm package-lock.json (or npm-shrinkwrap.json) file", + 'Pnpm import generates a pnpm-lock.yaml from an npm package-lock.json (or npm-shrinkwrap.json) file', }, { - name: ["rebuild", "rb"], + name: ['rebuild', 'rb'], description: `Rebuild a package`, args: [ { - name: "Package", - filterStrategy: "fuzzy", + name: 'Package', + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, - { template: "filepaths" }, + { template: 'filepaths' }, ], options: [ { - name: ["--recursive", "-r"], + name: ['--recursive', '-r'], description: `This command runs the pnpm rebuild command in every package of the monorepo`, }, FILTER_OPTION, ], }, { - name: "prune", + name: 'prune', description: `Removes unnecessary packages`, options: [ { - name: "--prod", + name: '--prod', description: `Remove the packages specified in devDependencies`, }, { - name: "--no-optional", + name: '--no-optional', description: `Remove the packages specified in optionalDependencies`, }, ], }, { - name: "fetch", + name: 'fetch', description: `EXPERIMENTAL FEATURE: Fetch packages from a lockfile into virtual store, package manifest is ignored: https://pnpm.io/cli/fetch`, options: [ { - name: "--prod", + name: '--prod', description: `Development packages will not be fetched`, }, { - name: "--dev", + name: '--dev', description: `Only development packages will be fetched`, }, ], }, { - name: "patch", + name: 'patch', description: `This command will cause a package to be extracted in a temporary directory intended to be editable at will`, args: { - name: "package", + name: 'package', generators: generatorInstalledPackages, }, options: [ { - name: "--edit-dir", + name: '--edit-dir', description: `The package that needs to be patched will be extracted to this directory`, }, ], }, { - name: "patch-commit", + name: 'patch-commit', args: { - name: "dir", + name: 'dir', }, description: `Generate a patch out of a directory`, }, { - name: "patch-remove", + name: 'patch-remove', args: { - name: "package", + name: 'package', isVariadic: true, // TODO: would be nice to have a generator of all patched packages }, @@ -479,68 +479,68 @@ This is similar to yarn unlink, except pnpm re-installs the dependency after rem const SUBCOMMANDS_RUN_SCRIPTS: Fig.Subcommand[] = [ { - name: ["run", "run-script"], - description: "Runs a script defined in the package's manifest file", + name: ['run', 'run-script'], + description: 'Runs a script defined in the package\'s manifest file', args: { - name: "Scripts", - filterStrategy: "fuzzy", + name: 'Scripts', + filterStrategy: 'fuzzy', generators: npmScriptsGenerator, isVariadic: true, }, options: [ { - name: ["-r", "--recursive"], - description: `This runs an arbitrary command from each package's "scripts" object. If a package doesn't have the command, it is skipped. If none of the packages have the command, the command fails`, + name: ['-r', '--recursive'], + description: `This runs an arbitrary command from each package's 'scripts' object. If a package doesn't have the command, it is skipped. If none of the packages have the command, the command fails`, }, { - name: "--if-present", + name: '--if-present', description: - "You can use the --if-present flag to avoid exiting with a non-zero exit code when the script is undefined. This lets you run potentially undefined scripts without breaking the execution chain", + 'You can use the --if-present flag to avoid exiting with a non-zero exit code when the script is undefined. This lets you run potentially undefined scripts without breaking the execution chain', }, { - name: "--parallel", + name: '--parallel', description: - "Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process", + 'Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process', }, { - name: "--stream", + name: '--stream', description: - "Stream output from child processes immediately, prefixed with the originating package directory. This allows output from different packages to be interleaved", + 'Stream output from child processes immediately, prefixed with the originating package directory. This allows output from different packages to be interleaved', }, FILTER_OPTION, ], }, { - name: "exec", + name: 'exec', description: `Execute a shell command in scope of a project. node_modules/.bin is added to the PATH, so pnpm exec allows executing commands of dependencies`, args: { - name: "Scripts", - filterStrategy: "fuzzy", + name: 'Scripts', + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, options: [ { - name: ["-r", "--recursive"], + name: ['-r', '--recursive'], description: `Execute the shell command in every project of the workspace. The name of the current package is available through the environment variable PNPM_PACKAGE_NAME (supported from pnpm v2.22.0 onwards)`, }, { - name: "--parallel", + name: '--parallel', description: - "Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process", + 'Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process', }, FILTER_OPTION, ], }, { - name: ["test", "t", "tst"], + name: ['test', 't', 'tst'], description: `Runs an arbitrary command specified in the package's test property of its scripts object. The intended usage of the property is to specify a command that runs unit or integration testing for your program`, }, { - name: "start", + name: 'start', description: `Runs an arbitrary command specified in the package's start property of its scripts object. If no start property is specified on the scripts object, it will attempt to run node server.js as a default, failing if neither are present. The intended usage of the property is to specify a command that starts your program`, }, @@ -548,7 +548,7 @@ The intended usage of the property is to specify a command that starts your prog const SUBCOMMANDS_REVIEW_DEPS: Fig.Subcommand[] = [ { - name: "audit", + name: 'audit', description: `Checks for known security issues with the installed packages. If security issues are found, try to update your dependencies via pnpm update. If a simple update does not fix all the issues, use overrides to force versions that are not vulnerable. @@ -556,161 +556,161 @@ For instance, if lodash@<2.1.0 is vulnerable, use overrides to force lodash@^2.1 Details at: https://pnpm.io/cli/audit`, options: [ { - name: "--audit-level", + name: '--audit-level', description: `Only print advisories with severity greater than or equal to `, args: { - name: "Audit Level", - default: "low", - suggestions: ["low", "moderate", "high", "critical"], + name: 'Audit Level', + default: 'low', + suggestions: ['low', 'moderate', 'high', 'critical'], }, }, { - name: "--fix", + name: '--fix', description: - "Add overrides to the package.json file in order to force non-vulnerable versions of the dependencies", + 'Add overrides to the package.json file in order to force non-vulnerable versions of the dependencies', }, { - name: "--json", + name: '--json', description: `Output audit report in JSON format`, }, { - name: ["--dev", "-D"], + name: ['--dev', '-D'], description: `Only audit dev dependencies`, }, { - name: ["--prod", "-P"], + name: ['--prod', '-P'], description: `Only audit production dependencies`, }, { - name: "--no-optional", + name: '--no-optional', description: `Don't audit optionalDependencies`, }, { - name: "--ignore-registry-errors", + name: '--ignore-registry-errors', description: `If the registry responds with a non-200 status code, the process should exit with 0. So the process will fail only if the registry actually successfully responds with found vulnerabilities`, }, ], }, { - name: ["list", "ls"], + name: ['list', 'ls'], description: `This command will output all the versions of packages that are installed, as well as their dependencies, in a tree-structure. -Positional arguments are name-pattern@version-range identifiers, which will limit the results to only the packages named. For example, pnpm list "babel-*" "eslint-*" semver@5`, +Positional arguments are name-pattern@version-range identifiers, which will limit the results to only the packages named. For example, pnpm list 'babel-*' 'eslint-*' semver@5`, options: [ { - name: ["--recursive", "-r"], + name: ['--recursive', '-r'], description: `Perform command on every package in subdirectories or on every workspace package, when executed inside a workspace`, }, { - name: "--json", + name: '--json', description: `Log output in JSON format`, }, { - name: "--long", + name: '--long', description: `Show extended information`, }, { - name: "--parseable", + name: '--parseable', description: `Outputs package directories in a parseable format instead of their tree view`, }, { - name: "--global", + name: '--global', description: `List packages in the global install directory instead of in the current project`, }, { - name: "--depth", + name: '--depth', description: `Max display depth of the dependency tree. pnpm ls --depth 0 will list direct dependencies only. pnpm ls --depth -1 will list projects only. Useful inside a workspace when used with the -r option`, - args: { name: "number" }, + args: { name: 'number' }, }, { - name: ["--dev", "-D"], + name: ['--dev', '-D'], description: `Only list dev dependencies`, }, { - name: ["--prod", "-P"], + name: ['--prod', '-P'], description: `Only list production dependencies`, }, { - name: "--no-optional", + name: '--no-optional', description: `Don't list optionalDependencies`, }, FILTER_OPTION, ], }, { - name: "outdated", + name: 'outdated', description: `Checks for outdated packages. The check can be limited to a subset of the installed packages by providing arguments (patterns are supported)`, options: [ { - name: ["--recursive", "-r"], + name: ['--recursive', '-r'], description: `Check for outdated dependencies in every package found in subdirectories, or in every workspace package when executed inside a workspace`, }, { - name: "--long", + name: '--long', description: `Print details`, }, { - name: "--global", + name: '--global', description: `List outdated global packages`, }, { - name: "--no-table", + name: '--no-table', description: `Prints the outdated dependencies in a list format instead of the default table. Good for small consoles`, }, { - name: "--compatible", + name: '--compatible', description: `Prints only versions that satisfy specifications in package.json`, }, { - name: ["--dev", "-D"], + name: ['--dev', '-D'], description: `Only list dev dependencies`, }, { - name: ["--prod", "-P"], + name: ['--prod', '-P'], description: `Only list production dependencies`, }, { - name: "--no-optional", + name: '--no-optional', description: `Doesn't check optionalDependencies`, }, ], }, { - name: "why", + name: 'why', description: `Shows all packages that depend on the specified package`, args: { - name: "Scripts", - filterStrategy: "fuzzy", + name: 'Scripts', + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, options: [ { - name: ["--recursive", "-r"], + name: ['--recursive', '-r'], description: `Show the dependency tree for the specified package on every package in subdirectories or on every workspace package when executed inside a workspace`, }, { - name: "--json", + name: '--json', description: `Log output in JSON format`, }, { - name: "--long", + name: '--long', description: `Show verbose output`, }, { - name: "--parseable", + name: '--parseable', description: `Show parseable output instead of tree view`, }, { - name: "--global", + name: '--global', description: `List packages in the global install directory instead of in the current project`, }, { - name: ["--dev", "-D"], + name: ['--dev', '-D'], description: `Only display the dependency tree for packages in devDependencies`, }, { - name: ["--prod", "-P"], + name: ['--prod', '-P'], description: `Only display the dependency tree for packages in dependencies`, }, FILTER_OPTION, @@ -720,176 +720,176 @@ pnpm ls --depth 0 will list direct dependencies only. pnpm ls --depth -1 will li const SUBCOMMANDS_MISC: Fig.Subcommand[] = [ { - name: "publish", + name: 'publish', description: `Publishes a package to the registry. When publishing a package inside a workspace, the LICENSE file from the root of the workspace is packed with the package (unless the package has a license of its own). You may override some fields before publish, using the publishConfig field in package.json. You also can use the publishConfig.directory to customize the published subdirectory (usually using third party build tools). When running this command recursively (pnpm -r publish), pnpm will publish all the packages that have versions not yet published to the registry`, args: { - name: "Branch", + name: 'Branch', generators: searchBranches, }, options: [ { - name: "--tag", + name: '--tag', description: `Publishes the package with the given tag. By default, pnpm publish updates the latest tag`, args: { - name: "", + name: '', }, }, { - name: "--dry-run", + name: '--dry-run', description: `Does everything a publish would do except actually publishing to the registry`, }, { - name: "--ignore-scripts", + name: '--ignore-scripts', description: `Ignores any publish related lifecycle scripts (prepublishOnly, postpublish, and the like)`, }, { - name: "--no-git-checks", + name: '--no-git-checks', description: `Don't check if current branch is your publish branch, clean, and up-to-date`, }, { - name: "--access", + name: '--access', description: `Tells the registry whether the published package should be public or restricted`, args: { - name: "Type", - suggestions: ["public", "private"], + name: 'Type', + suggestions: ['public', 'private'], }, }, { - name: "--force", + name: '--force', description: `Try to publish packages even if their current version is already found in the registry`, }, { - name: "--report-summary", + name: '--report-summary', description: `Save the list of published packages to pnpm-publish-summary.json. Useful when some other tooling is used to report the list of published packages`, }, FILTER_OPTION, ], }, { - name: ["recursive", "m", "multi", "-r"], + name: ['recursive', 'm', 'multi', '-r'], description: `Runs a pnpm command recursively on all subdirectories in the package or every available workspace`, options: [ { - name: "--link-workspace-packages", + name: '--link-workspace-packages', description: `Link locally available packages in workspaces of a monorepo into node_modules instead of re-downloading them from the registry. This emulates functionality similar to yarn workspaces. When this is set to deep, local packages can also be linked to subdependencies. Be advised that it is encouraged instead to use npmrc for this setting, to enforce the same behaviour in all environments. This option exists solely so you may override that if necessary`, args: { - name: "bool or `deep`", - suggestions: ["dee["], + name: 'bool or `deep`', + suggestions: ['dee['], }, }, { - name: "--workspace-concurrency", + name: '--workspace-concurrency', description: `Set the maximum number of tasks to run simultaneously. For unlimited concurrency use Infinity`, - args: { name: "" }, + args: { name: '' }, }, { - name: "--bail", + name: '--bail', description: `Stops when a task throws an error`, }, { - name: "--no-bail", + name: '--no-bail', description: `Don't stop when a task throws an error`, }, { - name: "--sort", + name: '--sort', description: `Packages are sorted topologically (dependencies before dependents)`, }, { - name: "--no-sort", + name: '--no-sort', description: `Disable packages sorting`, }, { - name: "--reverse", + name: '--reverse', description: `The order of packages is reversed`, }, FILTER_OPTION, ], }, { - name: "server", + name: 'server', description: `Manage a store server`, subcommands: [ { - name: "start", + name: 'start', description: - "Starts a server that performs all interactions with the store. Other commands will delegate any store-related tasks to this server", + 'Starts a server that performs all interactions with the store. Other commands will delegate any store-related tasks to this server', options: [ { - name: "--background", + name: '--background', description: `Runs the server in the background, similar to daemonizing on UNIX systems`, }, { - name: "--network-concurrency", + name: '--network-concurrency', description: `The maximum number of network requests to process simultaneously`, - args: { name: "number" }, + args: { name: 'number' }, }, { - name: "--protocol", + name: '--protocol', description: `The communication protocol used by the server. When this is set to auto, IPC is used on all systems except for Windows, which uses TCP`, args: { - name: "Type", - suggestions: ["auto", "tcp", "ipc"], + name: 'Type', + suggestions: ['auto', 'tcp', 'ipc'], }, }, { - name: "--port", + name: '--port', description: `The port number to use when TCP is used for communication. If a port is specified and the protocol is set to auto, regardless of system type, the protocol is automatically set to use TCP`, - args: { name: "port number" }, + args: { name: 'port number' }, }, { - name: "--store-dir", + name: '--store-dir', description: `The directory to use for the content addressable store`, - args: { name: "Path", template: "filepaths" }, + args: { name: 'Path', template: 'filepaths' }, }, { - name: "--lock", + name: '--lock', description: `Set to make the package store immutable to external processes while the server is running or not`, }, { - name: "--no-lock", + name: '--no-lock', description: `Set to make the package store mutable to external processes while the server is running or not`, }, { - name: "--ignore-stop-requests", + name: '--ignore-stop-requests', description: `Prevents you from stopping the server using pnpm server stop`, }, { - name: "--ignore-upload-requests", + name: '--ignore-upload-requests', description: `Prevents creating a new side effect cache during install`, }, ], }, { - name: "stop", - description: "Stops the store server", + name: 'stop', + description: 'Stops the store server', }, { - name: "status", - description: "Prints information about the running server", + name: 'status', + description: 'Prints information about the running server', }, ], }, { - name: "store", - description: "Managing the package store", + name: 'store', + description: 'Managing the package store', subcommands: [ { - name: "status", + name: 'status', description: `Checks for modified packages in the store. Returns exit code 0 if the content of the package is the same as it was at the time of unpacking`, }, { - name: "add", + name: 'add', description: `Functionally equivalent to pnpm add, except this adds new packages to the store directly without modifying any projects or files outside of the store`, }, { - name: "prune", + name: 'prune', description: `Removes orphan packages from the store. Pruning the store will save disk space, however may slow down future installations involving pruned packages. Ultimately, it is a safe operation, however not recommended if you have orphaned packages from a package you intend to reinstall. @@ -897,19 +897,19 @@ Please read the FAQ for more information on unreferenced packages and best pract Please note that this is prohibited when a store server is running`, }, { - name: "path", + name: 'path', description: `Returns the path to the active store directory`, }, ], }, { - name: "init", + name: 'init', description: - "Creates a basic package.json file in the current directory, if it doesn't exist already", + 'Creates a basic package.json file in the current directory, if it doesn\'t exist already', }, { - name: "doctor", - description: "Checks for known common issues with pnpm configuration", + name: 'doctor', + description: 'Checks for known common issues with pnpm configuration', }, ]; @@ -921,19 +921,19 @@ const subcommands = [ ]; const recursiveSubcommandsNames = [ - "add", - "exec", - "install", - "list", - "outdated", - "publish", - "rebuild", - "remove", - "run", - "test", - "unlink", - "update", - "why", + 'add', + 'exec', + 'install', + 'list', + 'outdated', + 'publish', + 'rebuild', + 'remove', + 'run', + 'test', + 'unlink', + 'update', + 'why', ]; const recursiveSubcommands = subcommands.filter((subcommand) => { @@ -951,46 +951,46 @@ SUBCOMMANDS_MISC[1].subcommands = recursiveSubcommands; // common options const COMMON_OPTIONS: Fig.Option[] = [ { - name: ["-C", "--dir"], + name: ['-C', '--dir'], args: { - name: "path", - template: "folders", + name: 'path', + template: 'folders', }, isPersistent: true, description: - "Run as if pnpm was started in instead of the current working directory", + 'Run as if pnpm was started in instead of the current working directory', }, { - name: ["-w", "--workspace-root"], + name: ['-w', '--workspace-root'], args: { - name: "workspace", + name: 'workspace', }, isPersistent: true, description: - "Run as if pnpm was started in the root of the instead of the current working directory", + 'Run as if pnpm was started in the root of the instead of the current working directory', }, { - name: ["-h", "--help"], + name: ['-h', '--help'], isPersistent: true, - description: "Output usage information", + description: 'Output usage information', }, { - name: ["-v", "--version"], - description: "Show pnpm's version", + name: ['-v', '--version'], + description: 'Show pnpm\'s version', }, ]; // SPEC const completionSpec: Fig.Spec = { - name: "pnpm", - description: "Fast, disk space efficient package manager", + name: 'pnpm', + description: 'Fast, disk space efficient package manager', args: { - name: "Scripts", - filterStrategy: "fuzzy", + name: 'Scripts', + filterStrategy: 'fuzzy', generators: npmScriptsGenerator, isVariadic: true, }, - filterStrategy: "fuzzy", + filterStrategy: 'fuzzy', generateSpec: async (tokens, executeShellCommand) => { const { script, postProcess } = dependenciesGenerator as Fig.Generator & { script: string[]; @@ -1017,13 +1017,13 @@ const completionSpec: Fig.Spec = { .map((name) => ({ name, loadSpec: name, - icon: "fig://icon?type=package", + icon: 'fig://icon?type=package', })); return { - name: "pnpm", + name: 'pnpm', subcommands, - } as Fig.Spec; + }; }, subcommands, options: COMMON_OPTIONS, From 25a617ed559d0bbe98205d7d0dafbeb8c7811079 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:33:39 -0800 Subject: [PATCH 10/25] Remove emoji icons from pnpm --- extensions/terminal-suggest/src/completions/pnpm.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/terminal-suggest/src/completions/pnpm.ts b/extensions/terminal-suggest/src/completions/pnpm.ts index 55dade961d5..12b71e358e1 100644 --- a/extensions/terminal-suggest/src/completions/pnpm.ts +++ b/extensions/terminal-suggest/src/completions/pnpm.ts @@ -32,7 +32,6 @@ const searchBranches: Fig.Generator = { return { name: elm.replace('*', '').trim(), description: 'Current branch', - icon: '⭐️', }; } else if (parts[0] === '+') { // Branch checked out in another worktree. From 0be0a9efb8a34c1b4f547d8b6d941694c438e002 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:36:05 -0800 Subject: [PATCH 11/25] Double to single quotes in yarn --- .../terminal-suggest/src/completions/yarn.ts | 1387 +++++++++-------- 1 file changed, 694 insertions(+), 693 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/yarn.ts b/extensions/terminal-suggest/src/completions/yarn.ts index a0bbbcc0a8e..8cf579f7722 100644 --- a/extensions/terminal-suggest/src/completions/yarn.ts +++ b/extensions/terminal-suggest/src/completions/yarn.ts @@ -3,21 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { npmScriptsGenerator, npmSearchGenerator } from "./npm"; +import { npmScriptsGenerator, npmSearchGenerator } from './npm'; -export const yarnScriptParserDirectives: Fig.Arg["parserDirectives"] = { +export const yarnScriptParserDirectives: Fig.Arg['parserDirectives'] = { alias: async (token, executeShellCommand) => { const npmPrefix = await executeShellCommand({ - command: "npm", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["prefix"], + command: 'npm', + args: ['prefix'], }); if (npmPrefix.status !== 0) { - throw new Error("npm prefix command failed"); + throw new Error('npm prefix command failed'); } const packageJson = await executeShellCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays + command: 'cat', args: [`${npmPrefix.stdout.trim()}/package.json`], }); const script: string = JSON.parse(packageJson.stdout).scripts?.[token]; @@ -29,51 +27,52 @@ export const yarnScriptParserDirectives: Fig.Arg["parserDirectives"] = { }; export const nodeClis = new Set([ - "vue", - "vite", - "nuxt", - "react-native", - "degit", - "expo", - "jest", - "next", - "electron", - "prisma", - "eslint", - "prettier", - "tsc", - "typeorm", - "babel", - "remotion", - "autocomplete-tools", - "redwood", - "rw", - "create-completion-spec", - "publish-spec-to-team", - "capacitor", - "cap", + 'vue', + 'vite', + 'nuxt', + 'react-native', + 'degit', + 'expo', + 'jest', + 'next', + 'electron', + 'prisma', + 'eslint', + 'prettier', + 'tsc', + 'typeorm', + 'babel', + 'remotion', + 'autocomplete-tools', + 'redwood', + 'rw', + 'create-completion-spec', + 'publish-spec-to-team', + 'capacitor', + 'cap', ]); // generate global package list from global package.json file const getGlobalPackagesGenerator: Fig.Generator = { custom: async (tokens, executeCommand, generatorContext) => { const { stdout: yarnGlobalDir } = await executeCommand({ - command: "yarn", - args: ["global", "dir"], + command: 'yarn', + args: ['global', 'dir'], }); const { stdout } = await executeCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays + command: 'cat', args: [`${yarnGlobalDir.trim()}/package.json`], }); - if (stdout.trim() == "") return []; + if (stdout.trim() === '') { + return []; + } try { const packageContent = JSON.parse(stdout); - const dependencyScripts = packageContent["dependencies"] || {}; - const devDependencyScripts = packageContent["devDependencies"] || {}; + const dependencyScripts = packageContent['dependencies'] || {}; + const devDependencyScripts = packageContent['devDependencies'] || {}; const dependencies = [ ...Object.keys(dependencyScripts), ...Object.keys(devDependencyScripts), @@ -85,7 +84,7 @@ const getGlobalPackagesGenerator: Fig.Generator = { return filteredDependencies.map((dependencyName) => ({ name: dependencyName, - icon: "📦", + icon: '📦', })); } catch (e) { } @@ -95,16 +94,18 @@ const getGlobalPackagesGenerator: Fig.Generator = { // generate package list of direct and indirect dependencies const allDependenciesGenerator: Fig.Generator = { - script: ["yarn", "list", "--depth=0", "--json"], + script: ['yarn', 'list', '--depth=0', '--json'], postProcess: (out) => { - if (out.trim() == "") return []; + if (out.trim() === '') { + return []; + } try { const packageContent = JSON.parse(out); const dependencies = packageContent.data.trees; return dependencies.map((dependency: { name: string }) => ({ - name: dependency.name.split("@")[0], - icon: "📦", + name: dependency.name.split('@')[0], + icon: '📦', })); } catch (e) { } return []; @@ -112,22 +113,22 @@ const allDependenciesGenerator: Fig.Generator = { }; const configList: Fig.Generator = { - script: ["yarn", "config", "list"], + script: ['yarn', 'config', 'list'], postProcess: function (out) { - if (out.trim() == "") { + if (out.trim() === '') { return []; } try { - const startIndex = out.indexOf("{"); - const endIndex = out.indexOf("}"); + const startIndex = out.indexOf('{'); + const endIndex = out.indexOf('}'); let output = out.substring(startIndex, endIndex + 1); // TODO: fix hacky code // reason: JSON parse was not working without double quotes output = output - .replace(/\'/gi, '"') - .replace("lastUpdateCheck", '"lastUpdateCheck"') - .replace("registry", '"lastUpdateCheck"'); + .replace(/\'/gi, '\'') + .replace('lastUpdateCheck', '\'lastUpdateCheck\'') + .replace('registry', '\'lastUpdateCheck\''); const configObject = JSON.parse(output); if (configObject) { return Object.keys(configObject).map((key) => ({ name: key })); @@ -140,20 +141,20 @@ const configList: Fig.Generator = { export const dependenciesGenerator: Fig.Generator = { script: [ - "bash", - "-c", - "until [[ -f package.json ]] || [[ $PWD = '/' ]]; do cd ..; done; cat package.json", + 'bash', + '-c', + 'until [[ -f package.json ]] || [[ $PWD = \' / \' ]]; do cd ..; done; cat package.json', ], postProcess: function (out, context = []) { - if (out.trim() === "") { + if (out.trim() === '') { return []; } try { const packageContent = JSON.parse(out); - const dependencies = packageContent["dependencies"] ?? {}; - const devDependencies = packageContent["devDependencies"]; - const optionalDependencies = packageContent["optionalDependencies"] ?? {}; + const dependencies = packageContent['dependencies'] ?? {}; + const devDependencies = packageContent['devDependencies']; + const optionalDependencies = packageContent['optionalDependencies'] ?? {}; Object.assign(dependencies, devDependencies, optionalDependencies); return Object.keys(dependencies) @@ -163,12 +164,12 @@ export const dependenciesGenerator: Fig.Generator = { }) .map((pkgName) => ({ name: pkgName, - icon: "📦", + icon: '📦', description: dependencies[pkgName] - ? "dependency" + ? 'dependency' : optionalDependencies[pkgName] - ? "optionalDependency" - : "devDependency", + ? 'optionalDependency' + : 'devDependency', })); } catch (e) { console.error(e); @@ -178,191 +179,195 @@ export const dependenciesGenerator: Fig.Generator = { }; const commonOptions: Fig.Option[] = [ - { name: ["-s", "--silent"], description: "Skip Yarn console logs" }, + { name: ['-s', '--silent'], description: 'Skip Yarn console logs' }, { - name: "--no-default-rc", + name: '--no-default-rc', description: - "Prevent Yarn from automatically detecting yarnrc and npmrc files", + 'Prevent Yarn from automatically detecting yarnrc and npmrc files', }, { - name: "--use-yarnrc", + name: '--use-yarnrc', description: - "Specifies a yarnrc file that Yarn should use (.yarnrc only, not .npmrc) (default: )", - args: { name: "path", template: "filepaths" }, + 'Specifies a yarnrc file that Yarn should use (.yarnrc only, not .npmrc) (default: )', + args: { name: 'path', template: 'filepaths' }, }, { - name: "--verbose", - description: "Output verbose messages on internal operations", + name: '--verbose', + description: 'Output verbose messages on internal operations', }, { - name: "--offline", + name: '--offline', description: - "Trigger an error if any required dependencies are not available in local cache", + 'Trigger an error if any required dependencies are not available in local cache', }, { - name: "--prefer-offline", + name: '--prefer-offline', description: - "Use network only if dependencies are not available in local cache", + 'Use network only if dependencies are not available in local cache', }, { - name: ["--enable-pnp", "--pnp"], - description: "Enable the Plug'n'Play installation", + name: ['--enable-pnp', '--pnp'], + description: 'Enable the Plug\'n\'Play installation', }, { - name: "--json", - description: "Format Yarn log messages as lines of JSON", + name: '--json', + description: 'Format Yarn log messages as lines of JSON', }, { - name: "--ignore-scripts", - description: "Don't run lifecycle scripts", + name: '--ignore-scripts', + description: 'Don\'t run lifecycle scripts', }, - { name: "--har", description: "Save HAR output of network traffic" }, - { name: "--ignore-platform", description: "Ignore platform checks" }, - { name: "--ignore-engines", description: "Ignore engines check" }, + { name: '--har', description: 'Save HAR output of network traffic' }, + { name: '--ignore-platform', description: 'Ignore platform checks' }, + { name: '--ignore-engines', description: 'Ignore engines check' }, { - name: "--ignore-optional", - description: "Ignore optional dependencies", + name: '--ignore-optional', + description: 'Ignore optional dependencies', }, { - name: "--force", + name: '--force', description: - "Install and build packages even if they were built before, overwrite lockfile", + 'Install and build packages even if they were built before, overwrite lockfile', }, { - name: "--skip-integrity-check", - description: "Run install without checking if node_modules is installed", + name: '--skip-integrity-check', + description: 'Run install without checking if node_modules is installed', }, { - name: "--check-files", - description: "Install will verify file tree of packages for consistency", + name: '--check-files', + description: 'Install will verify file tree of packages for consistency', }, { - name: "--no-bin-links", - description: "Don't generate bin links when setting up packages", + name: '--no-bin-links', + description: 'Don\'t generate bin links when setting up packages', }, - { name: "--flat", description: "Only allow one version of a package" }, + { name: '--flat', description: 'Only allow one version of a package' }, { - name: ["--prod", "--production"], + name: ['--prod', '--production'], description: - "Instruct Yarn to ignore NODE_ENV and take its production-or-not status from this flag instead", + 'Instruct Yarn to ignore NODE_ENV and take its production-or-not status from this flag instead', }, { - name: "--no-lockfile", - description: "Don't read or generate a lockfile", - }, - { name: "--pure-lockfile", description: "Don't generate a lockfile" }, - { - name: "--frozen-lockfile", - description: "Don't generate a lockfile and fail if an update is needed", + name: '--no-lockfile', + description: 'Don\'t read or generate a lockfile', }, { - name: "--update-checksums", - description: "Update package checksums from current repository", + name: '--pure-lockfile', description: 'Don\'t generate a lockfile' }, { - name: "--link-duplicates", - description: "Create hardlinks to the repeated modules in node_modules", + name: '--frozen-lockfile', + description: 'Don\'t generate a lockfile and fail if an update is needed', }, { - name: "--link-folder", - description: "Specify a custom folder to store global links", - args: { name: "path", template: "folders" }, + name: '--update-checksums', + description: 'Update package checksums from current repository', }, { - name: "--global-folder", - description: "Specify a custom folder to store global packages", - args: { name: "path", template: "folders" }, + name: '--link-duplicates', + description: 'Create hardlinks to the repeated modules in node_modules', }, { - name: "--modules-folder", + name: '--link-folder', + description: 'Specify a custom folder to store global links', + args: { name: 'path', template: 'folders' }, + }, + { + name: '--global-folder', + description: 'Specify a custom folder to store global packages', + args: { name: 'path', template: 'folders' }, + }, + { + name: '--modules-folder', description: - "Rather than installing modules into the node_modules folder relative to the cwd, output them here", - args: { name: "path", template: "folders" }, + 'Rather than installing modules into the node_modules folder relative to the cwd, output them here', + args: { name: 'path', template: 'folders' }, }, { - name: "--preferred-cache-folder", - description: "Specify a custom folder to store the yarn cache if possible", - args: { name: "path", template: "folders" }, + name: '--preferred-cache-folder', + description: 'Specify a custom folder to store the yarn cache if possible', + args: { name: 'path', template: 'folders' }, }, { - name: "--cache-folder", + name: '--cache-folder', description: - "Specify a custom folder that must be used to store the yarn cache", - args: { name: "path", template: "folders" }, + 'Specify a custom folder that must be used to store the yarn cache', + args: { name: 'path', template: 'folders' }, }, { - name: "--mutex", - description: "Use a mutex to ensure only one yarn instance is executing", - args: { name: "type[:specifier]" }, + name: '--mutex', + description: 'Use a mutex to ensure only one yarn instance is executing', + args: { name: 'type[:specifier]' }, }, { - name: "--emoji", - description: "Enables emoji in output", + name: '--emoji', + description: 'Enables emoji in output', args: { - default: "true", - suggestions: ["true", "false"], + default: 'true', + suggestions: ['true', 'false'], }, }, { - name: "--cwd", - description: "Working directory to use", - args: { name: "cwd", template: "folders" }, + name: '--cwd', + description: 'Working directory to use', + args: { name: 'cwd', template: 'folders' }, }, { - name: ["--proxy", "--https-proxy"], - description: "", - args: { name: "host" }, + name: ['--proxy', '--https-proxy'], + description: '', + args: { name: 'host' }, }, { - name: "--registry", - description: "Override configuration registry", - args: { name: "url" }, + name: '--registry', + description: 'Override configuration registry', + args: { name: 'url' }, }, - { name: "--no-progress", description: "Disable progress bar" }, + { name: '--no-progress', description: 'Disable progress bar' }, { - name: "--network-concurrency", - description: "Maximum number of concurrent network requests", - args: { name: "number" }, + name: '--network-concurrency', + description: 'Maximum number of concurrent network requests', + args: { name: 'number' }, }, { - name: "--network-timeout", - description: "TCP timeout for network requests", - args: { name: "milliseconds" }, + name: '--network-timeout', + description: 'TCP timeout for network requests', + args: { name: 'milliseconds' }, }, { - name: "--non-interactive", - description: "Do not show interactive prompts", + name: '--non-interactive', + description: 'Do not show interactive prompts', }, { - name: "--scripts-prepend-node-path", - description: "Prepend the node executable dir to the PATH in scripts", + name: '--scripts-prepend-node-path', + description: 'Prepend the node executable dir to the PATH in scripts', }, { - name: "--no-node-version-check", + name: '--no-node-version-check', description: - "Do not warn when using a potentially unsupported Node version", + 'Do not warn when using a potentially unsupported Node version', }, { - name: "--focus", + name: '--focus', description: - "Focus on a single workspace by installing remote copies of its sibling workspaces", + 'Focus on a single workspace by installing remote copies of its sibling workspaces', }, { - name: "--otp", - description: "One-time password for two factor authentication", - args: { name: "otpcode" }, + name: '--otp', + description: 'One-time password for two factor authentication', + args: { name: 'otpcode' }, }, ]; export const createCLIsGenerator: Fig.Generator = { script: function (context) { - if (context[context.length - 1] === "") return undefined; - const searchTerm = "create-" + context[context.length - 1]; + if (context[context.length - 1] === '') { + return undefined; + } + const searchTerm = 'create-' + context[context.length - 1]; return [ - "curl", - "-s", - "-H", - "Accept: application/json", + 'curl', + '-s', + '-H', + 'Accept: application/json', `https://api.npms.io/v2/search?q=${searchTerm}&size=20`, ]; }, @@ -371,13 +376,10 @@ export const createCLIsGenerator: Fig.Generator = { }, postProcess: function (out) { try { - return JSON.parse(out).results.map( - (item: { package: { name: string; description: string } }) => - ({ - name: item.package.name.substring(7), - description: item.package.description, - }) as Fig.Suggestion - ) as Fig.Suggestion[]; + return JSON.parse(out).results.map((item: { package: { name: string; description: string } }) => ({ + name: item.package.name.substring(7), + description: item.package.description, + })) as Fig.Suggestion[]; } catch (e) { return []; } @@ -385,271 +387,271 @@ export const createCLIsGenerator: Fig.Generator = { }; const completionSpec: Fig.Spec = { - name: "yarn", - description: "Manage packages and run scripts", + name: 'yarn', + description: 'Manage packages and run scripts', generateSpec: async (tokens, executeShellCommand) => { const binaries = ( await executeShellCommand({ - command: "bash", + command: 'bash', args: [ - "-c", + '-c', `until [[ -d node_modules/ ]] || [[ $PWD = '/' ]]; do cd ..; done; ls -1 node_modules/.bin/`, ], }) - ).stdout.split("\n"); + ).stdout.split('\n'); const subcommands = binaries .filter((name) => nodeClis.has(name)) .map((name) => ({ name: name, - loadSpec: name === "rw" ? "redwood" : name, - icon: "fig://icon?type=package", + loadSpec: name === 'rw' ? 'redwood' : name, + icon: 'fig://icon?type=package', })); return { - name: "yarn", + name: 'yarn', subcommands, - } as Fig.Spec; + }; }, args: { generators: npmScriptsGenerator, - filterStrategy: "fuzzy", + filterStrategy: 'fuzzy', parserDirectives: yarnScriptParserDirectives, isOptional: true, isCommand: true, }, options: [ { - name: "--disable-pnp", - description: "Disable the Plug'n'Play installation", + name: '--disable-pnp', + description: 'Disable the Plug\'n\'Play installation', }, { - name: "--emoji", - description: "Enable emoji in output (default: true)", + name: '--emoji', + description: 'Enable emoji in output (default: true)', args: { - name: "bool", - suggestions: [{ name: "true" }, { name: "false" }], + name: 'bool', + suggestions: [{ name: 'true' }, { name: 'false' }], }, }, { - name: ["--enable-pnp", "--pnp"], - description: "Enable the Plug'n'Play installation", + name: ['--enable-pnp', '--pnp'], + description: 'Enable the Plug\'n\'Play installation', }, { - name: "--flat", - description: "Only allow one version of a package", + name: '--flat', + description: 'Only allow one version of a package', }, { - name: "--focus", + name: '--focus', description: - "Focus on a single workspace by installing remote copies of its sibling workspaces", + 'Focus on a single workspace by installing remote copies of its sibling workspaces', }, { - name: "--force", + name: '--force', description: - "Install and build packages even if they were built before, overwrite lockfile", + 'Install and build packages even if they were built before, overwrite lockfile', }, { - name: "--frozen-lockfile", - description: "Don't generate a lockfile and fail if an update is needed", + name: '--frozen-lockfile', + description: 'Don\'t generate a lockfile and fail if an update is needed', }, { - name: "--global-folder", - description: "Specify a custom folder to store global packages", + name: '--global-folder', + description: 'Specify a custom folder to store global packages', args: { - template: "folders", + template: 'folders', }, }, { - name: "--har", - description: "Save HAR output of network traffic", + name: '--har', + description: 'Save HAR output of network traffic', }, { - name: "--https-proxy", - description: "", + name: '--https-proxy', + description: '', args: { - name: "path", - suggestions: [{ name: "https://" }], + name: 'path', + suggestions: [{ name: 'https://' }], }, }, { - name: "--ignore-engines", - description: "Ignore engines check", + name: '--ignore-engines', + description: 'Ignore engines check', }, { - name: "--ignore-optional", - description: "Ignore optional dependencies", + name: '--ignore-optional', + description: 'Ignore optional dependencies', }, { - name: "--ignore-platform", - description: "Ignore platform checks", + name: '--ignore-platform', + description: 'Ignore platform checks', }, { - name: "--ignore-scripts", - description: "Don't run lifecycle scripts", + name: '--ignore-scripts', + description: 'Don\'t run lifecycle scripts', }, { - name: "--json", + name: '--json', description: - "Format Yarn log messages as lines of JSON (see jsonlines.org)", + 'Format Yarn log messages as lines of JSON (see jsonlines.org)', }, { - name: "--link-duplicates", - description: "Create hardlinks to the repeated modules in node_modules", + name: '--link-duplicates', + description: 'Create hardlinks to the repeated modules in node_modules', }, { - name: "--link-folder", - description: "Specify a custom folder to store global links", + name: '--link-folder', + description: 'Specify a custom folder to store global links', args: { - template: "folders", + template: 'folders', }, }, { - name: "--modules-folder", + name: '--modules-folder', description: - "Rather than installing modules into the node_modules folder relative to the cwd, output them here", + 'Rather than installing modules into the node_modules folder relative to the cwd, output them here', args: { - template: "folders", + template: 'folders', }, }, { - name: "--mutex", - description: "Use a mutex to ensure only one yarn instance is executing", + name: '--mutex', + description: 'Use a mutex to ensure only one yarn instance is executing', args: [ { - name: "type", - suggestions: [{ name: ":" }], + name: 'type', + suggestions: [{ name: ':' }], }, { - name: "specifier", - suggestions: [{ name: ":" }], + name: 'specifier', + suggestions: [{ name: ':' }], }, ], }, { - name: "--network-concurrency", - description: "Maximum number of concurrent network requests", + name: '--network-concurrency', + description: 'Maximum number of concurrent network requests', args: { - name: "number", + name: 'number', }, }, { - name: "--network-timeout", - description: "TCP timeout for network requests", + name: '--network-timeout', + description: 'TCP timeout for network requests', args: { - name: "milliseconds", + name: 'milliseconds', }, }, { - name: "--no-bin-links", - description: "Don't generate bin links when setting up packages", + name: '--no-bin-links', + description: 'Don\'t generate bin links when setting up packages', }, { - name: "--no-default-rc", + name: '--no-default-rc', description: - "Prevent Yarn from automatically detecting yarnrc and npmrc files", + 'Prevent Yarn from automatically detecting yarnrc and npmrc files', }, { - name: "--no-lockfile", - description: "Don't read or generate a lockfile", + name: '--no-lockfile', + description: 'Don\'t read or generate a lockfile', }, { - name: "--non-interactive", - description: "Do not show interactive prompts", + name: '--non-interactive', + description: 'Do not show interactive prompts', }, { - name: "--no-node-version-check", + name: '--no-node-version-check', description: - "Do not warn when using a potentially unsupported Node version", + 'Do not warn when using a potentially unsupported Node version', }, { - name: "--no-progress", - description: "Disable progress bar", + name: '--no-progress', + description: 'Disable progress bar', }, { - name: "--offline", + name: '--offline', description: - "Trigger an error if any required dependencies are not available in local cache", + 'Trigger an error if any required dependencies are not available in local cache', }, { - name: "--otp", - description: "One-time password for two factor authentication", + name: '--otp', + description: 'One-time password for two factor authentication', args: { - name: "otpcode", + name: 'otpcode', }, }, { - name: "--prefer-offline", + name: '--prefer-offline', description: - "Use network only if dependencies are not available in local cache", + 'Use network only if dependencies are not available in local cache', }, { - name: "--preferred-cache-folder", + name: '--preferred-cache-folder', description: - "Specify a custom folder to store the yarn cache if possible", + 'Specify a custom folder to store the yarn cache if possible', args: { - template: "folders", + template: 'folders', }, }, { - name: ["--prod", "--production"], - description: "", + name: ['--prod', '--production'], + description: '', args: {}, }, { - name: "--proxy", - description: "", + name: '--proxy', + description: '', args: { - name: "host", + name: 'host', }, }, { - name: "--pure-lockfile", - description: "Don't generate a lockfile", + name: '--pure-lockfile', + description: 'Don\'t generate a lockfile', }, { - name: "--registry", - description: "Override configuration registry", + name: '--registry', + description: 'Override configuration registry', args: { - name: "url", + name: 'url', }, }, { - name: ["-s", "--silent"], + name: ['-s', '--silent'], description: - "Skip Yarn console logs, other types of logs (script output) will be printed", + 'Skip Yarn console logs, other types of logs (script output) will be printed', }, { - name: "--scripts-prepend-node-path", - description: "Prepend the node executable dir to the PATH in scripts", + name: '--scripts-prepend-node-path', + description: 'Prepend the node executable dir to the PATH in scripts', args: { - suggestions: [{ name: "true" }, { name: "false" }], + suggestions: [{ name: 'true' }, { name: 'false' }], }, }, { - name: "--skip-integrity-check", - description: "Run install without checking if node_modules is installed", + name: '--skip-integrity-check', + description: 'Run install without checking if node_modules is installed', }, { - name: "--strict-semver", - description: "", + name: '--strict-semver', + description: '', }, ...commonOptions, { - name: ["-v", "--version"], - description: "Output the version number", + name: ['-v', '--version'], + description: 'Output the version number', }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], subcommands: [ { - name: "add", - description: "Installs a package and any packages that it depends on", + name: 'add', + description: 'Installs a package and any packages that it depends on', args: { - name: "package", + name: 'package', generators: npmSearchGenerator, debounce: true, isVariadic: true, @@ -657,138 +659,138 @@ const completionSpec: Fig.Spec = { options: [ ...commonOptions, { - name: ["-W", "--ignore-workspace-root-check"], - description: "Required to run yarn add inside a workspace root", + name: ['-W', '--ignore-workspace-root-check'], + description: 'Required to run yarn add inside a workspace root', }, { - name: ["-D", "--dev"], - description: "Save package to your `devDependencies`", + name: ['-D', '--dev'], + description: 'Save package to your `devDependencies`', }, { - name: ["-P", "--peer"], - description: "Save package to your `peerDependencies`", + name: ['-P', '--peer'], + description: 'Save package to your `peerDependencies`', }, { - name: ["-O", "--optional"], - description: "Save package to your `optionalDependencies`", + name: ['-O', '--optional'], + description: 'Save package to your `optionalDependencies`', }, { - name: ["-E", "--exact"], - description: "Install exact version", - dependsOn: ["--latest"], + name: ['-E', '--exact'], + description: 'Install exact version', + dependsOn: ['--latest'], }, { - name: ["-T", "--tilde"], + name: ['-T', '--tilde'], description: - "Install most recent release with the same minor version", + 'Install most recent release with the same minor version', }, { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "audit", + name: 'audit', description: - "Perform a vulnerability audit against the installed packages", + 'Perform a vulnerability audit against the installed packages', options: [ { - name: "--summary", - description: "Only print the summary", + name: '--summary', + description: 'Only print the summary', }, { - name: "--groups", + name: '--groups', description: - "Only audit dependencies from listed groups. Default: devDependencies, dependencies, optionalDependencies", + 'Only audit dependencies from listed groups. Default: devDependencies, dependencies, optionalDependencies', args: { - name: "group_name", + name: 'group_name', isVariadic: true, }, }, { - name: "--level", + name: '--level', description: - "Only print advisories with severity greater than or equal to one of the following: info|low|moderate|high|critical. Default: info", + 'Only print advisories with severity greater than or equal to one of the following: info|low|moderate|high|critical. Default: info', args: { - name: "severity", + name: 'severity', suggestions: [ - { name: "info" }, - { name: "low" }, - { name: "moderate" }, - { name: "high" }, - { name: "critical" }, + { name: 'info' }, + { name: 'low' }, + { name: 'moderate' }, + { name: 'high' }, + { name: 'critical' }, ], }, }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "autoclean", + name: 'autoclean', description: - "Cleans and removes unnecessary files from package dependencies", + 'Cleans and removes unnecessary files from package dependencies', options: [ { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, { - name: ["-i", "--init"], + name: ['-i', '--init'], description: - "Creates the .yarnclean file if it does not exist, and adds the default entries", + 'Creates the .yarnclean file if it does not exist, and adds the default entries', }, { - name: ["-f", "--force"], - description: "If a .yarnclean file exists, run the clean process", + name: ['-f', '--force'], + description: 'If a .yarnclean file exists, run the clean process', }, ], }, { - name: "bin", - description: "Displays the location of the yarn bin folder", + name: 'bin', + description: 'Displays the location of the yarn bin folder', options: [ { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "cache", - description: "Yarn cache list will print out every cached package", + name: 'cache', + description: 'Yarn cache list will print out every cached package', options: [ ...commonOptions, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], subcommands: [ { - name: "clean", - description: "Clear global cache", + name: 'clean', + description: 'Clear global cache', }, { - name: "dir", - description: "Print yarn’s global cache path", + name: 'dir', + description: 'Print yarn’s global cache path', }, { - name: "list", - description: "Print out every cached package", + name: 'list', + description: 'Print out every cached package', options: [ { - name: "--pattern", - description: "Filter cached packages by pattern", + name: '--pattern', + description: 'Filter cached packages by pattern', args: { - name: "pattern", + name: 'pattern', }, }, ], @@ -796,204 +798,204 @@ const completionSpec: Fig.Spec = { ], }, { - name: "config", - description: "Configure yarn", + name: 'config', + description: 'Configure yarn', options: [ { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], subcommands: [ { - name: "set", - description: "Sets the config key to a certain value", + name: 'set', + description: 'Sets the config key to a certain value', options: [ { - name: ["-g", "--global"], - description: "Set global config", + name: ['-g', '--global'], + description: 'Set global config', }, ], }, { - name: "get", - description: "Print the value for a given key", + name: 'get', + description: 'Print the value for a given key', args: { generators: configList, }, }, { - name: "delete", - description: "Deletes a given key from the config", + name: 'delete', + description: 'Deletes a given key from the config', args: { generators: configList, }, }, { - name: "list", - description: "Displays the current configuration", + name: 'list', + description: 'Displays the current configuration', }, ], }, { - name: "create", - description: "Creates new projects from any create-* starter kits", + name: 'create', + description: 'Creates new projects from any create-* starter kits', args: { - name: "cli", + name: 'cli', generators: createCLIsGenerator, loadSpec: async (token) => ({ - name: "create-" + token, - type: "global", + name: 'create-' + token, + type: 'global', }), isCommand: true, }, options: [ ...commonOptions, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "exec", - description: "", + name: 'exec', + description: '', options: [ { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "generate-lock-entry", - description: "Generates a lock file entry", + name: 'generate-lock-entry', + description: 'Generates a lock file entry', options: [ { - name: "--use-manifest", + name: '--use-manifest', description: - "Specify which manifest file to use for generating lock entry", + 'Specify which manifest file to use for generating lock entry', args: { - template: "filepaths", + template: 'filepaths', }, }, { - name: "--resolved", - description: "Generate from <*.tgz>#", + name: '--resolved', + description: 'Generate from <*.tgz>#', args: { - template: "filepaths", + template: 'filepaths', }, }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "global", - description: "Manage yarn globally", + name: 'global', + description: 'Manage yarn globally', subcommands: [ { - name: "add", - description: "Install globally packages on your operating system", + name: 'add', + description: 'Install globally packages on your operating system', args: { - name: "package", + name: 'package', generators: npmSearchGenerator, debounce: true, isVariadic: true, }, }, { - name: "bin", - description: "Displays the location of the yarn global bin folder", + name: 'bin', + description: 'Displays the location of the yarn global bin folder', }, { - name: "dir", + name: 'dir', description: - "Displays the location of the global installation folder", + 'Displays the location of the global installation folder', }, { - name: "ls", - description: "List globally installed packages (deprecated)", + name: 'ls', + description: 'List globally installed packages (deprecated)', }, { - name: "list", - description: "List globally installed packages", + name: 'list', + description: 'List globally installed packages', }, { - name: "remove", - description: "Remove globally installed packages", + name: 'remove', + description: 'Remove globally installed packages', args: { - name: "package", - filterStrategy: "fuzzy", + name: 'package', + filterStrategy: 'fuzzy', generators: getGlobalPackagesGenerator, isVariadic: true, }, options: [ ...commonOptions, { - name: ["-W", "--ignore-workspace-root-check"], + name: ['-W', '--ignore-workspace-root-check'], description: - "Required to run yarn remove inside a workspace root", + 'Required to run yarn remove inside a workspace root', }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "upgrade", - description: "Upgrade globally installed packages", + name: 'upgrade', + description: 'Upgrade globally installed packages', options: [ ...commonOptions, { - name: ["-S", "--scope"], - description: "Upgrade packages under the specified scope", - args: { name: "scope" }, + name: ['-S', '--scope'], + description: 'Upgrade packages under the specified scope', + args: { name: 'scope' }, }, { - name: ["-L", "--latest"], - description: "List the latest version of packages", + name: ['-L', '--latest'], + description: 'List the latest version of packages', }, { - name: ["-E", "--exact"], + name: ['-E', '--exact'], description: - "Install exact version. Only used when --latest is specified", - dependsOn: ["--latest"], + 'Install exact version. Only used when --latest is specified', + dependsOn: ['--latest'], }, { - name: ["-P", "--pattern"], - description: "Upgrade packages that match pattern", - args: { name: "pattern" }, + name: ['-P', '--pattern'], + description: 'Upgrade packages that match pattern', + args: { name: 'pattern' }, }, { - name: ["-T", "--tilde"], + name: ['-T', '--tilde'], description: - "Install most recent release with the same minor version. Only used when --latest is specified", + 'Install most recent release with the same minor version. Only used when --latest is specified', }, { - name: ["-C", "--caret"], + name: ['-C', '--caret'], description: - "Install most recent release with the same major version. Only used when --latest is specified", - dependsOn: ["--latest"], + 'Install most recent release with the same major version. Only used when --latest is specified', + dependsOn: ['--latest'], }, { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', }, - { name: ["-h", "--help"], description: "Output usage information" }, + { name: ['-h', '--help'], description: 'Output usage information' }, ], }, { - name: "upgrade-interactive", + name: 'upgrade-interactive', description: - "Display the outdated packages before performing any upgrade", + 'Display the outdated packages before performing any upgrade', options: [ { - name: "--latest", - description: "Use the version tagged latest in the registry", + name: '--latest', + description: 'Use the version tagged latest in the registry', }, ], }, @@ -1001,533 +1003,532 @@ const completionSpec: Fig.Spec = { options: [ ...commonOptions, { - name: "--prefix", - description: "Bin prefix to use to install binaries", + name: '--prefix', + description: 'Bin prefix to use to install binaries', args: { - name: "prefix", + name: 'prefix', }, }, { - name: "--latest", - description: "Bin prefix to use to install binaries", + name: '--latest', + description: 'Bin prefix to use to install binaries', }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "help", - description: "Output usage information", + name: 'help', + description: 'Output usage information', }, { - name: "import", - description: "Generates yarn.lock from an npm package-lock.json file", + name: 'import', + description: 'Generates yarn.lock from an npm package-lock.json file', }, { - name: "info", - description: "Show information about a package", + name: 'info', + description: 'Show information about a package', }, { - name: "init", - description: "Interactively creates or updates a package.json file", + name: 'init', + description: 'Interactively creates or updates a package.json file', options: [ ...commonOptions, { - name: ["-y", "--yes"], - description: "Use default options", + name: ['-y', '--yes'], + description: 'Use default options', }, { - name: ["-p", "--private"], - description: "Use default options and private true", + name: ['-p', '--private'], + description: 'Use default options and private true', }, { - name: ["-i", "--install"], - description: "Install a specific Yarn release", + name: ['-i', '--install'], + description: 'Install a specific Yarn release', args: { - name: "version", + name: 'version', }, }, { - name: "-2", - description: "Generates the project using Yarn 2", + name: '-2', + description: 'Generates the project using Yarn 2', }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "install", - description: "Install all the dependencies listed within package.json", + name: 'install', + description: 'Install all the dependencies listed within package.json', options: [ ...commonOptions, { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "licenses", - description: "", + name: 'licenses', + description: '', subcommands: [ { - name: "list", - description: "List licenses for installed packages", + name: 'list', + description: 'List licenses for installed packages', }, { - name: "generate-disclaimer", - description: "List of licenses from all the packages", + name: 'generate-disclaimer', + description: 'List of licenses from all the packages', }, ], }, { - name: "link", - description: "Symlink a package folder during development", + name: 'link', + description: 'Symlink a package folder during development', args: { isOptional: true, - name: "package", + name: 'package', }, options: [ ...commonOptions, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "list", - description: "Lists all dependencies for the current working directory", + name: 'list', + description: 'Lists all dependencies for the current working directory', options: [ { - name: "--depth", - description: "Restrict the depth of the dependencies", + name: '--depth', + description: 'Restrict the depth of the dependencies', }, { - name: "--pattern", - description: "Filter the list of dependencies by the pattern", + name: '--pattern', + description: 'Filter the list of dependencies by the pattern', }, ], }, { - name: "login", - description: "Store registry username and email", + name: 'login', + description: 'Store registry username and email', }, { - name: "logout", - description: "Clear registry username and email", + name: 'logout', + description: 'Clear registry username and email', }, { - name: "node", - description: "", + name: 'node', + description: '', }, { - name: "outdated", - description: "Checks for outdated package dependencies", + name: 'outdated', + description: 'Checks for outdated package dependencies', options: [ ...commonOptions, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "owner", - description: "Manage package owners", + name: 'owner', + description: 'Manage package owners', subcommands: [ { - name: "list", - description: "Lists all of the owners of a package", + name: 'list', + description: 'Lists all of the owners of a package', args: { - name: "package", + name: 'package', }, }, { - name: "add", - description: "Adds the user as an owner of the package", + name: 'add', + description: 'Adds the user as an owner of the package', args: { - name: "package", + name: 'package', }, }, { - name: "remove", - description: "Removes the user as an owner of the package", + name: 'remove', + description: 'Removes the user as an owner of the package', args: [ { - name: "user", + name: 'user', }, { - name: "package", + name: 'package', }, ], }, ], }, { - name: "pack", - description: "Creates a compressed gzip archive of package dependencies", + name: 'pack', + description: 'Creates a compressed gzip archive of package dependencies', options: [ { - name: "--filename", + name: '--filename', description: - "Creates a compressed gzip archive of package dependencies and names the file filename", + 'Creates a compressed gzip archive of package dependencies and names the file filename', }, ], }, { - name: "policies", - description: "Defines project-wide policies for your project", + name: 'policies', + description: 'Defines project-wide policies for your project', subcommands: [ { - name: "set-version", - description: "Will download the latest stable release", + name: 'set-version', + description: 'Will download the latest stable release', options: [ { - name: "--rc", - description: "Download the latest rc release", + name: '--rc', + description: 'Download the latest rc release', }, ], }, ], }, { - name: "publish", - description: "Publishes a package to the npm registry", - args: { name: "Tarball or Folder", template: "folders" }, + name: 'publish', + description: 'Publishes a package to the npm registry', + args: { name: 'Tarball or Folder', template: 'folders' }, options: [ ...commonOptions, - { name: ["-h", "--help"], description: "Output usage information" }, + { name: ['-h', '--help'], description: 'Output usage information' }, { - name: "--major", - description: "Auto-increment major version number", + name: '--major', + description: 'Auto-increment major version number', }, { - name: "--minor", - description: "Auto-increment minor version number", + name: '--minor', + description: 'Auto-increment minor version number', }, { - name: "--patch", - description: "Auto-increment patch version number", + name: '--patch', + description: 'Auto-increment patch version number', }, { - name: "--premajor", - description: "Auto-increment premajor version number", + name: '--premajor', + description: 'Auto-increment premajor version number', }, { - name: "--preminor", - description: "Auto-increment preminor version number", + name: '--preminor', + description: 'Auto-increment preminor version number', }, { - name: "--prepatch", - description: "Auto-increment prepatch version number", + name: '--prepatch', + description: 'Auto-increment prepatch version number', }, { - name: "--prerelease", - description: "Auto-increment prerelease version number", + name: '--prerelease', + description: 'Auto-increment prerelease version number', }, { - name: "--preid", - description: "Add a custom identifier to the prerelease", - args: { name: "preid" }, + name: '--preid', + description: 'Add a custom identifier to the prerelease', + args: { name: 'preid' }, }, { - name: "--message", - description: "Message", - args: { name: "message" }, + name: '--message', + description: 'Message', + args: { name: 'message' }, }, - { name: "--no-git-tag-version", description: "No git tag version" }, + { name: '--no-git-tag-version', description: 'No git tag version' }, { - name: "--no-commit-hooks", - description: "Bypass git hooks when committing new version", + name: '--no-commit-hooks', + description: 'Bypass git hooks when committing new version', }, - { name: "--access", description: "Access", args: { name: "access" } }, - { name: "--tag", description: "Tag", args: { name: "tag" } }, + { name: '--access', description: 'Access', args: { name: 'access' } }, + { name: '--tag', description: 'Tag', args: { name: 'tag' } }, ], }, { - name: "remove", - description: "Remove installed package", + name: 'remove', + description: 'Remove installed package', args: { - filterStrategy: "fuzzy", + filterStrategy: 'fuzzy', generators: dependenciesGenerator, isVariadic: true, }, options: [ ...commonOptions, { - name: ["-W", "--ignore-workspace-root-check"], - description: "Required to run yarn remove inside a workspace root", + name: ['-W', '--ignore-workspace-root-check'], + description: 'Required to run yarn remove inside a workspace root', }, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, ], }, { - name: "run", - description: "Runs a defined package script", + name: 'run', + description: 'Runs a defined package script', options: [ ...commonOptions, - { name: ["-h", "--help"], description: "Output usage information" }, + { name: ['-h', '--help'], description: 'Output usage information' }, ], args: [ { - name: "script", - description: "Script to run from your package.json", + name: 'script', + description: 'Script to run from your package.json', generators: npmScriptsGenerator, - filterStrategy: "fuzzy", + filterStrategy: 'fuzzy', parserDirectives: yarnScriptParserDirectives, isCommand: true, }, { - name: "env", - suggestions: ["env"], - description: "Lists environment variables available to scripts", + name: 'env', + suggestions: ['env'], + description: 'Lists environment variables available to scripts', isOptional: true, }, ], }, { - name: "tag", - description: "Add, remove, or list tags on a package", + name: 'tag', + description: 'Add, remove, or list tags on a package', }, { - name: "team", - description: "Maintain team memberships", + name: 'team', + description: 'Maintain team memberships', subcommands: [ { - name: "create", - description: "Create a new team", + name: 'create', + description: 'Create a new team', args: { - name: "", + name: '', }, }, { - name: "destroy", - description: "Destroys an existing team", + name: 'destroy', + description: 'Destroys an existing team', args: { - name: "", + name: '', }, }, { - name: "add", - description: "Add a user to an existing team", + name: 'add', + description: 'Add a user to an existing team', args: [ { - name: "", + name: '', }, { - name: "", + name: '', }, ], }, { - name: "remove", - description: "Remove a user from a team they belong to", + name: 'remove', + description: 'Remove a user from a team they belong to', args: { - name: " ", + name: ' ', }, }, { - name: "list", + name: 'list', description: - "If performed on an organization name, will return a list of existing teams under that organization. If performed on a team, it will instead return a list of all users belonging to that particular team", + 'If performed on an organization name, will return a list of existing teams under that organization. If performed on a team, it will instead return a list of all users belonging to that particular team', args: { - name: "|", + name: '|', }, }, ], }, { - name: "unlink", - description: "Unlink a previously created symlink for a package", + name: 'unlink', + description: 'Unlink a previously created symlink for a package', }, { - name: "unplug", - description: "", + name: 'unplug', + description: '', }, { - name: "upgrade", + name: 'upgrade', description: - "Upgrades packages to their latest version based on the specified range", + 'Upgrades packages to their latest version based on the specified range', args: { - name: "package", + name: 'package', generators: dependenciesGenerator, - filterStrategy: "fuzzy", + filterStrategy: 'fuzzy', isVariadic: true, isOptional: true, }, options: [ ...commonOptions, { - name: ["-S", "--scope"], - description: "Upgrade packages under the specified scope", - args: { name: "scope" }, + name: ['-S', '--scope'], + description: 'Upgrade packages under the specified scope', + args: { name: 'scope' }, }, { - name: ["-L", "--latest"], - description: "List the latest version of packages", + name: ['-L', '--latest'], + description: 'List the latest version of packages', }, { - name: ["-E", "--exact"], + name: ['-E', '--exact'], description: - "Install exact version. Only used when --latest is specified", - dependsOn: ["--latest"], + 'Install exact version. Only used when --latest is specified', + dependsOn: ['--latest'], }, { - name: ["-P", "--pattern"], - description: "Upgrade packages that match pattern", - args: { name: "pattern" }, + name: ['-P', '--pattern'], + description: 'Upgrade packages that match pattern', + args: { name: 'pattern' }, }, { - name: ["-T", "--tilde"], + name: ['-T', '--tilde'], description: - "Install most recent release with the same minor version. Only used when --latest is specified", + 'Install most recent release with the same minor version. Only used when --latest is specified', }, { - name: ["-C", "--caret"], + name: ['-C', '--caret'], description: - "Install most recent release with the same major version. Only used when --latest is specified", - dependsOn: ["--latest"], + 'Install most recent release with the same major version. Only used when --latest is specified', + dependsOn: ['--latest'], }, { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', }, - { name: ["-h", "--help"], description: "Output usage information" }, + { name: ['-h', '--help'], description: 'Output usage information' }, ], }, { - name: "upgrade-interactive", - description: "Upgrades packages in interactive mode", + name: 'upgrade-interactive', + description: 'Upgrades packages in interactive mode', options: [ { - name: "--latest", - description: "Use the version tagged latest in the registry", + name: '--latest', + description: 'Use the version tagged latest in the registry', }, ], }, { - name: "version", - description: "Update version of your package", + name: 'version', + description: 'Update version of your package', options: [ ...commonOptions, - { name: ["-h", "--help"], description: "Output usage information" }, + { name: ['-h', '--help'], description: 'Output usage information' }, { - name: "--new-version", - description: "New version", - args: { name: "version" }, + name: '--new-version', + description: 'New version', + args: { name: 'version' }, }, { - name: "--major", - description: "Auto-increment major version number", + name: '--major', + description: 'Auto-increment major version number', }, { - name: "--minor", - description: "Auto-increment minor version number", + name: '--minor', + description: 'Auto-increment minor version number', }, { - name: "--patch", - description: "Auto-increment patch version number", + name: '--patch', + description: 'Auto-increment patch version number', }, { - name: "--premajor", - description: "Auto-increment premajor version number", + name: '--premajor', + description: 'Auto-increment premajor version number', }, { - name: "--preminor", - description: "Auto-increment preminor version number", + name: '--preminor', + description: 'Auto-increment preminor version number', }, { - name: "--prepatch", - description: "Auto-increment prepatch version number", + name: '--prepatch', + description: 'Auto-increment prepatch version number', }, { - name: "--prerelease", - description: "Auto-increment prerelease version number", + name: '--prerelease', + description: 'Auto-increment prerelease version number', }, { - name: "--preid", - description: "Add a custom identifier to the prerelease", - args: { name: "preid" }, + name: '--preid', + description: 'Add a custom identifier to the prerelease', + args: { name: 'preid' }, }, { - name: "--message", - description: "Message", - args: { name: "message" }, + name: '--message', + description: 'Message', + args: { name: 'message' }, }, - { name: "--no-git-tag-version", description: "No git tag version" }, + { name: '--no-git-tag-version', description: 'No git tag version' }, { - name: "--no-commit-hooks", - description: "Bypass git hooks when committing new version", + name: '--no-commit-hooks', + description: 'Bypass git hooks when committing new version', }, - { name: "--access", description: "Access", args: { name: "access" } }, - { name: "--tag", description: "Tag", args: { name: "tag" } }, + { name: '--access', description: 'Access', args: { name: 'access' } }, + { name: '--tag', description: 'Tag', args: { name: 'tag' } }, ], }, { - name: "versions", + name: 'versions', description: - "Displays version information of the currently installed Yarn, Node.js, and its dependencies", + 'Displays version information of the currently installed Yarn, Node.js, and its dependencies', }, { - name: "why", - description: "Show information about why a package is installed", + name: 'why', + description: 'Show information about why a package is installed', args: { - name: "package", - filterStrategy: "fuzzy", + name: 'package', + filterStrategy: 'fuzzy', generators: allDependenciesGenerator, }, options: [ ...commonOptions, { - name: ["-h", "--help"], - description: "Output usage information", + name: ['-h', '--help'], + description: 'Output usage information', }, { - name: "--peers", + name: '--peers', description: - "Print the peer dependencies that match the specified name", + 'Print the peer dependencies that match the specified name', }, { - name: ["-R", "--recursive"], + name: ['-R', '--recursive'], description: - "List, for each workspace, what are all the paths that lead to the dependency", + 'List, for each workspace, what are all the paths that lead to the dependency', }, ], }, { - name: "workspace", - description: "Manage workspace", - filterStrategy: "fuzzy", + name: 'workspace', + description: 'Manage workspace', + filterStrategy: 'fuzzy', generateSpec: async (_tokens, executeShellCommand) => { const version = ( await executeShellCommand({ - command: "yarn", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["--version"], + command: 'yarn', + args: ['--version'], }) ).stdout; - const isYarnV1 = version.startsWith("1."); + const isYarnV1 = version.startsWith('1.'); const getWorkspacesDefinitionsV1 = async () => { const { stdout } = await executeShellCommand({ - command: "yarn", - args: ["workspaces", "info"], + command: 'yarn', + args: ['workspaces', 'info'], }); - const startJson = stdout.indexOf("{"); - const endJson = stdout.lastIndexOf("}"); + const startJson = stdout.indexOf('{'); + const endJson = stdout.lastIndexOf('}'); return Object.entries( JSON.parse(stdout.slice(startJson, endJson + 1)) as Record< @@ -1545,11 +1546,11 @@ const completionSpec: Fig.Spec = { // yarn workspaces list --json const out = ( await executeShellCommand({ - command: "yarn", - args: ["workspaces", "list", "--json"], + command: 'yarn', + args: ['workspaces', 'list', '--json'], }) ).stdout; - return out.split("\n").map((line) => JSON.parse(line.trim())); + return out.split('\n').map((line) => JSON.parse(line.trim())); }; try { @@ -1562,22 +1563,22 @@ const completionSpec: Fig.Spec = { const subcommands: Fig.Subcommand[] = workspacesDefinitions.map( ({ name, location }: { name: string; location: string }) => ({ name, - description: "Workspaces", + description: 'Workspaces', args: { - name: "script", + name: 'script', generators: { cache: { - strategy: "stale-while-revalidate", + strategy: 'stale-while-revalidate', ttl: 60_000, // 60s }, - script: ["cat", `${location}/package.json`], + script: ['cat', `${location}/package.json`], postProcess: function (out: string) { - if (out.trim() == "") { + if (out.trim() === '') { return []; } try { const packageContent = JSON.parse(out); - const scripts = packageContent["scripts"]; + const scripts = packageContent['scripts']; if (scripts) { return Object.keys(scripts).map((script) => ({ name: script, @@ -1592,82 +1593,82 @@ const completionSpec: Fig.Spec = { ); return { - name: "workspace", + name: 'workspace', subcommands, }; } catch (e) { console.error(e); } - return { name: "workspaces" }; + return { name: 'workspaces' }; }, }, { - name: "workspaces", - description: "Show information about your workspaces", + name: 'workspaces', + description: 'Show information about your workspaces', options: [ { - name: "subcommand", - description: "", + name: 'subcommand', + description: '', args: { - suggestions: [{ name: "info" }, { name: "run" }], + suggestions: [{ name: 'info' }, { name: 'run' }], }, }, { - name: "flags", - description: "", + name: 'flags', + description: '', }, ], }, { - name: "set", - description: "Set global Yarn options", + name: 'set', + description: 'Set global Yarn options', subcommands: [ { - name: "resolution", - description: "Enforce a package resolution", + name: 'resolution', + description: 'Enforce a package resolution', args: [ { - name: "descriptor", + name: 'descriptor', description: - "A descriptor for the package, in the form of 'lodash@npm:^1.2.3'", + 'A descriptor for the package, in the form of \'lodash@npm:^ 1.2.3\'', }, { - name: "resolution", - description: "The version of the package to resolve", + name: 'resolution', + description: 'The version of the package to resolve', }, ], options: [ { - name: ["-s", "--save"], + name: ['-s', '--save'], description: - "Persist the resolution inside the top-level manifest", + 'Persist the resolution inside the top-level manifest', }, ], }, { - name: "version", - description: "Lock the Yarn version used by the project", + name: 'version', + description: 'Lock the Yarn version used by the project', args: { - name: "version", + name: 'version', description: - "Use the specified version, which can also be a Yarn 2 build (e.g 2.0.0-rc.30) or a Yarn 1 build (e.g 1.22.1)", - template: "filepaths", + 'Use the specified version, which can also be a Yarn 2 build (e.g 2.0.0-rc.30) or a Yarn 1 build (e.g 1.22.1)', + template: 'filepaths', suggestions: [ { - name: "from-sources", - insertValue: "from sources", + name: 'from-sources', + insertValue: 'from sources', }, - "latest", - "canary", - "classic", - "self", + 'latest', + 'canary', + 'classic', + 'self', ], }, options: [ { - name: "--only-if-needed", + name: '--only-if-needed', description: - "Only lock the Yarn version if it isn't already locked", + 'Only lock the Yarn version if it isn\'t already locked', }, ], }, From 88132c29dae3fa77bfef447daecb59df87d969b7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:36:14 -0800 Subject: [PATCH 12/25] Fix remaining hygiene issues in yarn spec --- extensions/terminal-suggest/src/completions/yarn.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/extensions/terminal-suggest/src/completions/yarn.ts b/extensions/terminal-suggest/src/completions/yarn.ts index 8cf579f7722..7b0750ba2b1 100644 --- a/extensions/terminal-suggest/src/completions/yarn.ts +++ b/extensions/terminal-suggest/src/completions/yarn.ts @@ -84,7 +84,6 @@ const getGlobalPackagesGenerator: Fig.Generator = { return filteredDependencies.map((dependencyName) => ({ name: dependencyName, - icon: '📦', })); } catch (e) { } @@ -105,7 +104,6 @@ const allDependenciesGenerator: Fig.Generator = { const dependencies = packageContent.data.trees; return dependencies.map((dependency: { name: string }) => ({ name: dependency.name.split('@')[0], - icon: '📦', })); } catch (e) { } return []; @@ -164,7 +162,6 @@ export const dependenciesGenerator: Fig.Generator = { }) .map((pkgName) => ({ name: pkgName, - icon: '📦', description: dependencies[pkgName] ? 'dependency' : optionalDependencies[pkgName] @@ -780,7 +777,7 @@ const completionSpec: Fig.Spec = { }, { name: 'dir', - description: 'Print yarn’s global cache path', + description: 'Print yarn\'s global cache path', }, { name: 'list', From ae22fa2c65d3c03b45cc1e0e8c1e6feedd648009 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Sat, 20 Dec 2025 11:10:13 -0800 Subject: [PATCH 13/25] Add status updates for completed steps in Getting Started page (#284565) --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index df48ad5fd1b..8a6f2eae905 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -5,6 +5,7 @@ import { $, Dimension, addDisposableListener, append, clearNode, reset } from '../../../../base/browser/dom.js'; import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js'; +import { status } from '../../../../base/browser/ui/aria/aria.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; @@ -295,6 +296,9 @@ export class GettingStartedPage extends EditorPane { badgeelement.setAttribute('aria-label', localize('stepNotDone', "Checkbox for Step {0}: Not completed", step.title)); } }); + if (step.done) { + status(localize('stepAutoCompleted', "Step {0} completed", step.title)); + } } this.updateCategoryProgress(); })); From 792929f336c2b89b8a53f5d01b11ce04f0a442ac Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 20 Dec 2025 16:31:46 -0800 Subject: [PATCH 14/25] Get chat session transferring working on the ChatSessionStore (#283512) * Get chat session transferring working on the ChatSessionStore * fix comment * Fix tests, validate location * Tests * IChatTransferredSessionData is just a URI * Fix leak --- .../api/browser/mainThreadChatAgents2.ts | 5 +- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostChatAgents2.ts | 4 +- .../chat/browser/actions/chatActions.ts | 6 +- .../contrib/chat/browser/chatViewPane.ts | 31 +- .../contrib/chat/common/chatService.ts | 12 +- .../contrib/chat/common/chatServiceImpl.ts | 91 ++--- .../contrib/chat/common/chatSessionStore.ts | 231 +++++++---- .../chat/common/chatTransferService.ts | 4 +- .../workbench/contrib/chat/common/chatUri.ts | 4 + .../localAgentSessionsProvider.test.ts | 4 +- .../chat/test/common/chatService.test.ts | 2 + .../chat/test/common/chatSessionStore.test.ts | 374 ++++++++++++++++++ .../contrib/chat/test/common/mockChatModel.ts | 10 +- .../chat/test/common/mockChatService.ts | 6 +- .../test/browser/workbenchTestServices.ts | 92 +---- .../test/common/workbenchTestServices.ts | 104 ++++- .../vscode.proposed.interactive.d.ts | 2 +- 19 files changed, 720 insertions(+), 266 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/chatSessionStore.test.ts diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index e5a45b43d65..1099251114a 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -148,7 +148,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.deleteAndDispose(handle); } - $transferActiveChatSession(toWorkspace: UriComponents): void { + async $transferActiveChatSession(toWorkspace: UriComponents): Promise { const widget = this._chatWidgetService.lastFocusedWidget; const model = widget?.viewModel?.model; if (!model) { @@ -156,8 +156,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return; } - const location = widget.location; - this._chatService.transferChatSession({ sessionId: model.sessionId, inputState: model.inputModel.state.get(), location }, URI.revive(toWorkspace)); + await this._chatService.transferChatSession(model.sessionResource, URI.revive(toWorkspace)); } async $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): Promise { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8fc90fe1cb9..ccfd8731f6e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1469,7 +1469,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: interactive const interactive: typeof vscode.interactive = { - transferActiveChat(toWorkspace: vscode.Uri) { + transferActiveChat(toWorkspace: vscode.Uri): Thenable { checkProposedApiEnabled(extension, 'interactive'); return extHostChatAgents2.transferActiveChat(toWorkspace); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d24a2d7b932..11941ad8369 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1402,7 +1402,7 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; $unregisterAgent(handle: number): void; - $transferActiveChatSession(toWorkspace: UriComponents): void; + $transferActiveChatSession(toWorkspace: UriComponents): Promise; } export interface ICodeMapperTextEdit { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 5aa9eaa3398..047976cf458 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -437,8 +437,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }); } - transferActiveChat(newWorkspace: vscode.Uri): void { - this._proxy.$transferActiveChatSession(newWorkspace); + async transferActiveChat(newWorkspace: vscode.Uri): Promise { + await this._proxy.$transferActiveChatSession(newWorkspace); } createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index fa0a9b4bdb4..0095bf738f0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -310,13 +310,15 @@ abstract class OpenChatGlobalAction extends Action2 { let resp: Promise | undefined; if (opts?.query) { - chatWidget.setInput(opts.query); - if (!opts.isPartialQuery) { + if (opts.isPartialQuery) { + chatWidget.setInput(opts.query); + } else { if (!chatWidget.viewModel) { await Event.toPromise(chatWidget.onDidChangeViewModel); } await waitForDefaultAgent(chatAgentService, chatWidget.input.currentModeKind); + chatWidget.setInput(opts.query); // wait until the model is restored before setting the input, or it will be cleared when the model is restored resp = chatWidget.acceptInput(); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index b6a683b7ef2..02e29f23337 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -215,9 +215,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private onDidChangeAgents(): void { if (this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat)) { if (!this._widget?.viewModel && !this.restoringSession) { - const info = this.getTransferredOrPersistedSessionInfo(); + const sessionResource = this.getTransferredOrPersistedSessionInfo(); this.restoringSession = - (info.sessionId ? this.chatService.getOrRestoreSession(LocalChatSessionUri.forSession(info.sessionId)) : Promise.resolve(undefined)).then(async modelRef => { + (sessionResource ? this.chatService.getOrRestoreSession(sessionResource) : Promise.resolve(undefined)).then(async modelRef => { if (!this._widget) { return; // renderBody has not been called yet } @@ -228,9 +228,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const wasVisible = this._widget.visible; try { this._widget.setVisible(false); - if (info.inputState && modelRef) { - modelRef.object.inputModel.setState(info.inputState); - } await this.showModel(modelRef); } finally { @@ -245,16 +242,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._onDidChangeViewWelcomeState.fire(); } - private getTransferredOrPersistedSessionInfo(): { sessionId?: string; inputState?: IChatModelInputState; mode?: ChatModeKind } { - if (this.chatService.transferredSessionData?.location === ChatAgentLocation.Chat) { - const sessionId = this.chatService.transferredSessionData.sessionId; - return { - sessionId, - inputState: this.chatService.transferredSessionData.inputState, - }; + private getTransferredOrPersistedSessionInfo(): URI | undefined { + if (this.chatService.transferredSessionResource) { + return this.chatService.transferredSessionResource; } - return { sessionId: this.viewState.sessionId }; + return this.viewState.sessionId ? LocalChatSessionUri.forSession(this.viewState.sessionId) : undefined; } protected override renderBody(parent: HTMLElement): void { @@ -658,12 +651,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { //#region Model Management private async applyModel(): Promise { - const info = this.getTransferredOrPersistedSessionInfo(); - const modelRef = info.sessionId ? await this.chatService.getOrRestoreSession(LocalChatSessionUri.forSession(info.sessionId)) : undefined; - if (modelRef && info.inputState) { - modelRef.object.inputModel.setState(info.inputState); - } - + const sessionResource = this.getTransferredOrPersistedSessionInfo(); + const modelRef = sessionResource ? await this.chatService.getOrRestoreSession(sessionResource) : undefined; await this.showModel(modelRef); } @@ -673,8 +662,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let ref: IChatModelReference | undefined; if (startNewSession) { - ref = modelRef ?? (this.chatService.transferredSessionData?.sessionId && this.chatService.transferredSessionData?.location === ChatAgentLocation.Chat - ? await this.chatService.getOrRestoreSession(LocalChatSessionUri.forSession(this.chatService.transferredSessionData.sessionId)) + ref = modelRef ?? (this.chatService.transferredSessionResource + ? await this.chatService.getOrRestoreSession(this.chatService.transferredSessionResource) : this.chatService.startSession(ChatAgentLocation.Chat)); if (!ref) { throw new Error('Could not start chat session'); diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 289e4bcb22f..21f972a109b 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -23,7 +23,7 @@ import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { IWorkspaceSymbol } from '../../search/common/search.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from './chatAgents.js'; import { IChatEditingSession } from './chatEditingService.js'; -import { IChatModel, IChatModelInputState, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js'; +import { IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; import { IChatParserContext } from './chatRequestParser.js'; import { IChatRequestVariableEntry } from './chatVariableEntries.js'; @@ -934,12 +934,6 @@ export interface IChatProviderInfo { id: string; } -export interface IChatTransferredSessionData { - sessionId: string; - location: ChatAgentLocation; - inputState: IChatModelInputState | undefined; -} - export interface IChatSendRequestResponseState { responseCreatedPromise: Promise; responseCompletePromise: Promise; @@ -1006,7 +1000,7 @@ export const IChatService = createDecorator('IChatService'); export interface IChatService { _serviceBrand: undefined; - transferredSessionData: IChatTransferredSessionData | undefined; + transferredSessionResource: URI | undefined; readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }>; @@ -1066,7 +1060,7 @@ export interface IChatService { notifyUserAction(event: IChatUserActionEvent): void; readonly onDidDisposeSession: Event<{ readonly sessionResource: URI[]; readonly reason: 'cleared' }>; - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; + transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise; activateDefaultAgent(location: ChatAgentLocation): Promise; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index db08afa3591..da263d91534 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -14,6 +14,7 @@ import { Disposable, DisposableResourceMap, DisposableStore, IDisposable, Mutabl import { revive } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { autorun, derived, IObservable } from '../../../../base/common/observable.js'; +import { isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -24,7 +25,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { Progress } from '../../../../platform/progress/common/progress.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { InlineChatConfigKeys } from '../../inlineChat/common/inlineChat.js'; @@ -36,10 +37,10 @@ import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, ICha import { ChatModelStore, IStartSessionProps } from './chatModelStore.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from './chatParserTypes.js'; import { ChatRequestParser } from './chatRequestParser.js'; -import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatTransferredSessionData, IChatUserActionEvent, ResponseModelState } from './chatService.js'; +import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSessionsService } from './chatSessionsService.js'; -import { ChatSessionStore, IChatSessionEntryMetadata, IChatTransfer2 } from './chatSessionStore.js'; +import { ChatSessionStore, IChatSessionEntryMetadata } from './chatSessionStore.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatTransferService } from './chatTransferService.js'; import { LocalChatSessionUri } from './chatUri.js'; @@ -50,10 +51,6 @@ import { ILanguageModelToolsService } from './languageModelToolsService.js'; const serializedChatKey = 'interactive.sessions'; -const TransferredGlobalChatKey = 'chat.workspaceTransfer'; - -const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60; - class CancellableRequest implements IDisposable { constructor( public readonly cancellationTokenSource: CancellationTokenSource, @@ -82,9 +79,9 @@ export class ChatService extends Disposable implements IChatService { private _persistedSessions: ISerializableChatsData; private _saveModelsEnabled = true; - private _transferredSessionData: IChatTransferredSessionData | undefined; - public get transferredSessionData(): IChatTransferredSessionData | undefined { - return this._transferredSessionData; + private _transferredSessionResource: URI | undefined; + public get transferredSessionResource(): URI | undefined { + return this._transferredSessionResource; } private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>()); @@ -128,7 +125,7 @@ export class ChatService extends Disposable implements IChatService { } constructor( - @IStorageService private readonly storageService: IStorageService, + @IStorageService storageService: IStorageService, @ILogService private readonly logService: ILogService, @IExtensionService private readonly extensionService: IExtensionService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -175,21 +172,15 @@ export class ChatService extends Disposable implements IChatService { this._persistedSessions = {}; } - const transferredData = this.getTransferredSessionData(); - const transferredChat = transferredData?.chat; - if (transferredChat) { - this.trace('constructor', `Transferred session ${transferredChat.sessionId}`); - this._persistedSessions[transferredChat.sessionId] = transferredChat; - this._transferredSessionData = { - sessionId: transferredChat.sessionId, - location: transferredData.location, - inputState: transferredData.inputState - }; - } - this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore)); this._chatSessionStore.migrateDataIfNeeded(() => this._persistedSessions); + const transferredData = this._chatSessionStore.getTransferredSessionData(); + if (transferredData) { + this.trace('constructor', `Transferred session ${transferredData}`); + this._transferredSessionResource = transferredData; + } + // When using file storage, populate _persistedSessions with session metadata from the index // This ensures that getPersistedSessionTitle() can find titles for inactive sessions this.initializePersistedSessionsFromFileStorage().then(() => { @@ -309,23 +300,6 @@ export class ChatService extends Disposable implements IChatService { } } - private getTransferredSessionData(): IChatTransfer2 | undefined { - const data: IChatTransfer2[] = this.storageService.getObject(TransferredGlobalChatKey, StorageScope.PROFILE, []); - const workspaceUri = this.workspaceContextService.getWorkspace().folders[0]?.uri; - if (!workspaceUri) { - return; - } - - const thisWorkspace = workspaceUri.toString(); - const currentTime = Date.now(); - // Only use transferred data if it was created recently - const transferred = data.find(item => URI.revive(item.toWorkspace).toString() === thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS)); - // Keep data that isn't for the current workspace and that hasn't expired yet - const filtered = data.filter(item => URI.revive(item.toWorkspace).toString() !== thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS)); - this.storageService.store(TransferredGlobalChatKey, JSON.stringify(filtered), StorageScope.PROFILE, StorageTarget.MACHINE); - return transferred; - } - /** * todo@connor4312 This will be cleaned up with the globalization of edits. */ @@ -540,8 +514,9 @@ export class ChatService extends Disposable implements IChatService { } let sessionData: ISerializableChatData | undefined; - if (this.transferredSessionData?.sessionId === sessionId) { - sessionData = revive(this._persistedSessions[sessionId]); + if (isEqual(this.transferredSessionResource, sessionResource)) { + this._transferredSessionResource = undefined; + sessionData = revive(await this._chatSessionStore.readTransferredSession(sessionResource)); } else { sessionData = revive(await this._chatSessionStore.readSession(sessionId)); } @@ -558,11 +533,6 @@ export class ChatService extends Disposable implements IChatService { canUseTools: true, }); - const isTransferred = this.transferredSessionData?.sessionId === sessionId; - if (isTransferred) { - this._transferredSessionData = undefined; - } - return sessionRef; } @@ -1309,22 +1279,25 @@ export class ChatService extends Disposable implements IChatService { return this._chatSessionStore.hasSessions(); } - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { - const model = Iterable.find(this._sessionModels.values(), model => model.sessionId === transferredSessionData.sessionId); - if (!model) { - throw new Error(`Failed to transfer session. Unknown session ID: ${transferredSessionData.sessionId}`); + async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise { + if (!LocalChatSessionUri.isLocalSession(transferredSessionResource)) { + throw new Error(`Can only transfer local chat sessions. Invalid session: ${transferredSessionResource}`); } - const existingRaw: IChatTransfer2[] = this.storageService.getObject(TransferredGlobalChatKey, StorageScope.PROFILE, []); - existingRaw.push({ - chat: model.toJSON(), + const model = this._sessionModels.get(transferredSessionResource) as ChatModel | undefined; + if (!model) { + throw new Error(`Failed to transfer session. Unknown session: ${transferredSessionResource}`); + } + + if (model.initialLocation !== ChatAgentLocation.Chat) { + throw new Error(`Can only transfer chat sessions located in the Chat view. Session ${transferredSessionResource} has location=${model.initialLocation}`); + } + + await this._chatSessionStore.storeTransferSession({ + sessionResource: model.sessionResource, timestampInMilliseconds: Date.now(), toWorkspace: toWorkspace, - inputState: transferredSessionData.inputState, - location: transferredSessionData.location, - }); - - this.storageService.store(TransferredGlobalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE); + }, model); this.chatTransferService.addWorkspaceToTransferred(toWorkspace); this.trace('transferChatSession', `Transferred session ${model.sessionResource} to workspace ${toWorkspace.toString()}`); } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts index 1c52d67f4b8..47218ece5c1 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts @@ -19,10 +19,11 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { Dto } from '../../../services/extensions/common/proxyIdentifier.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { awaitStatsForSession } from './chat.js'; import { ModifiedFileEntryState } from './chatEditingService.js'; -import { ChatModel, IChatModelInputState, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData } from './chatModel.js'; +import { ChatModel, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData } from './chatModel.js'; import { IChatSessionStats, IChatSessionTiming, ResponseModelState } from './chatService.js'; import { LocalChatSessionUri } from './chatUri.js'; import { ChatAgentLocation } from './constants.js'; @@ -30,12 +31,12 @@ import { ChatAgentLocation } from './constants.js'; const maxPersistedSessions = 25; const ChatIndexStorageKey = 'chat.ChatSessionStore.index'; -// const ChatTransferIndexStorageKey = 'ChatSessionStore.transferIndex'; +const ChatTransferIndexStorageKey = 'ChatSessionStore.transferIndex'; export class ChatSessionStore extends Disposable { private readonly storageRoot: URI; private readonly previousEmptyWindowStorageRoot: URI | undefined; - // private readonly transferredSessionStorageRoot: URI; + private readonly transferredSessionStorageRoot: URI; private readonly storeQueue = new Sequencer(); @@ -65,8 +66,7 @@ export class ChatSessionStore extends Disposable { joinPath(this.environmentService.workspaceStorageHome, 'no-workspace', 'chatSessions') : undefined; - // TODO tmpdir - // this.transferredSessionStorageRoot = joinPath(this.environmentService.workspaceStorageHome, 'transferredChatSessions'); + this.transferredSessionStorageRoot = joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, 'transferredChatSessions'); this._register(this.lifecycleService.onWillShutdown(e => { this.shuttingDown = true; @@ -124,33 +124,124 @@ export class ChatSessionStore extends Disposable { } } - // async storeTransferSession(transferData: IChatTransfer, session: ISerializableChatData): Promise { - // try { - // const content = JSON.stringify(session, undefined, 2); - // await this.fileService.writeFile(this.transferredSessionStorageRoot, VSBuffer.fromString(content)); - // } catch (e) { - // this.reportError('sessionWrite', 'Error writing chat session', e); - // return; - // } + async storeTransferSession(transferData: IChatTransfer, session: ChatModel): Promise { + const index = this.getTransferredSessionIndex(); + const workspaceKey = transferData.toWorkspace.toString(); - // const index = this.getTransferredSessionIndex(); - // index[transferData.toWorkspace.toString()] = transferData; - // try { - // this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE); - // } catch (e) { - // this.reportError('storeTransferSession', 'Error storing chat transfer session', e); - // } - // } + // Clean up any preexisting transferred session for this workspace + const existingTransfer = index[workspaceKey]; + if (existingTransfer) { + try { + const existingSessionResource = URI.revive(existingTransfer.sessionResource); + if (existingSessionResource && LocalChatSessionUri.parseLocalSessionId(existingSessionResource)) { + const existingStorageLocation = this.getTransferredSessionStorageLocation(existingSessionResource); + await this.fileService.del(existingStorageLocation); + } + } catch (e) { + if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) { + this.reportError('storeTransferSession', 'Error deleting old transferred session file', e); + } + } + } - // private getTransferredSessionIndex(): IChatTransferIndex { - // try { - // const data: IChatTransferIndex = this.storageService.getObject(ChatTransferIndexStorageKey, StorageScope.PROFILE, {}); - // return data; - // } catch (e) { - // this.reportError('getTransferredSessionIndex', 'Error reading chat transfer index', e); - // return {}; - // } - // } + try { + const content = JSON.stringify(session, undefined, 2); + const storageLocation = this.getTransferredSessionStorageLocation(session.sessionResource); + await this.fileService.writeFile(storageLocation, VSBuffer.fromString(content)); + } catch (e) { + this.reportError('sessionWrite', 'Error writing chat session', e); + return; + } + + index[workspaceKey] = transferData; + try { + this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE); + } catch (e) { + this.reportError('storeTransferSession', 'Error storing chat transfer session', e); + } + } + + private getTransferredSessionIndex(): IChatTransferIndex { + try { + const data: IChatTransferIndex = this.storageService.getObject(ChatTransferIndexStorageKey, StorageScope.PROFILE, {}); + return data; + } catch (e) { + this.reportError('getTransferredSessionIndex', 'Error reading chat transfer index', e); + return {}; + } + } + + private static readonly TRANSFER_EXPIRATION_MS = 60 * 1000 * 5; + + getTransferredSessionData(): URI | undefined { + try { + const index = this.getTransferredSessionIndex(); + const workspaceFolders = this.workspaceContextService.getWorkspace().folders; + if (workspaceFolders.length !== 1) { + // Can only transfer sessions to single-folder workspaces + return undefined; + } + + const workspaceKey = workspaceFolders[0].uri.toString(); + const transferredSessionForWorkspace: IChatTransferDto = index[workspaceKey]; + if (!transferredSessionForWorkspace) { + return undefined; + } + + // Check if the transfer has expired + const revivedTransferData = revive(transferredSessionForWorkspace); + if (Date.now() - transferredSessionForWorkspace.timestampInMilliseconds > ChatSessionStore.TRANSFER_EXPIRATION_MS) { + this.logService.info('ChatSessionStore: Transferred session has expired'); + this.cleanupTransferredSession(revivedTransferData.sessionResource); + return undefined; + } + return !!LocalChatSessionUri.parseLocalSessionId(revivedTransferData.sessionResource) && revivedTransferData.sessionResource; + } catch (e) { + this.reportError('getTransferredSession', 'Error getting transferred chat session URI', e); + return undefined; + } + } + + async readTransferredSession(sessionResource: URI): Promise { + try { + const storageLocation = this.getTransferredSessionStorageLocation(sessionResource); + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (!sessionId) { + return undefined; + } + + const sessionData = await this.readSessionFromLocation(storageLocation, sessionId); + + // Clean up the transferred session after reading + await this.cleanupTransferredSession(sessionResource); + + return sessionData; + } catch (e) { + this.reportError('getTransferredSession', 'Error getting transferred chat session', e); + return undefined; + } + } + + private async cleanupTransferredSession(sessionResource: URI): Promise { + try { + // Remove from index + const index = this.getTransferredSessionIndex(); + const workspaceFolders = this.workspaceContextService.getWorkspace().folders; + if (workspaceFolders.length === 1) { + const workspaceKey = workspaceFolders[0].uri.toString(); + delete index[workspaceKey]; + this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE); + } + + // Delete the transferred session file + const storageLocation = this.getTransferredSessionStorageLocation(sessionResource); + await this.fileService.del(storageLocation); + } catch (e) { + if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) { + this.reportError('cleanupTransferredSession', 'Error cleaning up transferred session', e); + } + } + } private async writeSession(session: ChatModel | ISerializableChatData): Promise { try { @@ -359,45 +450,49 @@ export class ChatSessionStore extends Disposable { public async readSession(sessionId: string): Promise { return await this.storeQueue.queue(async () => { - let rawData: string | undefined; const storageLocation = this.getStorageLocation(sessionId); - try { - rawData = (await this.fileService.readFile(storageLocation)).value.toString(); - } catch (e) { - this.reportError('sessionReadFile', `Error reading chat session file ${sessionId}`, e); + return this.readSessionFromLocation(storageLocation, sessionId); + }); + } - if (toFileOperationResult(e) === FileOperationResult.FILE_NOT_FOUND && this.previousEmptyWindowStorageRoot) { - rawData = await this.readSessionFromPreviousLocation(sessionId); - } + private async readSessionFromLocation(storageLocation: URI, sessionId: string): Promise { + let rawData: string | undefined; + try { + rawData = (await this.fileService.readFile(storageLocation)).value.toString(); + } catch (e) { + this.reportError('sessionReadFile', `Error reading chat session file ${sessionId}`, e); - if (!rawData) { - return undefined; - } + if (toFileOperationResult(e) === FileOperationResult.FILE_NOT_FOUND && this.previousEmptyWindowStorageRoot) { + rawData = await this.readSessionFromPreviousLocation(sessionId); } - try { - // TODO Copied from ChatService.ts, cleanup - const session: ISerializableChatDataIn = revive(JSON.parse(rawData)); // Revive serialized URIs in session data - // Revive serialized markdown strings in response data - for (const request of session.requests) { - if (Array.isArray(request.response)) { - request.response = request.response.map((response) => { - if (typeof response === 'string') { - return new MarkdownString(response); - } - return response; - }); - } else if (typeof request.response === 'string') { - request.response = [new MarkdownString(request.response)]; - } - } - - return normalizeSerializableChatData(session); - } catch (err) { - this.reportError('malformedSession', `Malformed session data in ${storageLocation.fsPath}: [${rawData.substring(0, 20)}${rawData.length > 20 ? '...' : ''}]`, err); + if (!rawData) { return undefined; } - }); + } + + try { + // TODO Copied from ChatService.ts, cleanup + const session: ISerializableChatDataIn = revive(JSON.parse(rawData)); // Revive serialized URIs in session data + // Revive serialized markdown strings in response data + for (const request of session.requests) { + if (Array.isArray(request.response)) { + request.response = request.response.map((response) => { + if (typeof response === 'string') { + return new MarkdownString(response); + } + return response; + }); + } else if (typeof request.response === 'string') { + request.response = [new MarkdownString(request.response)]; + } + } + + return normalizeSerializableChatData(session); + } catch (err) { + this.reportError('malformedSession', `Malformed session data in ${storageLocation.fsPath}: [${rawData.substring(0, 20)}${rawData.length > 20 ? '...' : ''}]`, err); + return undefined; + } } private async readSessionFromPreviousLocation(sessionId: string): Promise { @@ -421,6 +516,11 @@ export class ChatSessionStore extends Disposable { return joinPath(this.storageRoot, `${chatSessionId}.json`); } + private getTransferredSessionStorageLocation(sessionResource: URI): URI { + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + return joinPath(this.transferredSessionStorageRoot, `${sessionId}.json`); + } + public getChatStorageFolder(): URI { return this.storageRoot; } @@ -525,18 +625,17 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P export interface IChatTransfer { toWorkspace: URI; + sessionResource: URI; timestampInMilliseconds: number; - inputState: IChatModelInputState | undefined; - location: ChatAgentLocation; } export interface IChatTransfer2 extends IChatTransfer { chat: ISerializableChatData; } -// type IChatTransferDto = Dto; +type IChatTransferDto = Dto; /** * Map of destination workspace URI to chat transfer data */ -// type IChatTransferIndex = Record; +type IChatTransferIndex = Record; diff --git a/src/vs/workbench/contrib/chat/common/chatTransferService.ts b/src/vs/workbench/contrib/chat/common/chatTransferService.ts index bbc21070343..2bd380085b2 100644 --- a/src/vs/workbench/contrib/chat/common/chatTransferService.ts +++ b/src/vs/workbench/contrib/chat/common/chatTransferService.ts @@ -30,7 +30,7 @@ export class ChatTransferService implements IChatTransferService { @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { } - deleteWorkspaceFromTransferredList(workspace: URI): void { + private deleteWorkspaceFromTransferredList(workspace: URI): void { const transferredWorkspaces = this.storageService.getObject(transferredWorkspacesKey, StorageScope.PROFILE, []); const updatedWorkspaces = transferredWorkspaces.filter(uri => uri !== workspace.toString()); this.storageService.store(transferredWorkspacesKey, updatedWorkspaces, StorageScope.PROFILE, StorageTarget.MACHINE); @@ -54,7 +54,7 @@ export class ChatTransferService implements IChatTransferService { } } - isChatTransferredWorkspace(workspace: URI, storageService: IStorageService): boolean { + private isChatTransferredWorkspace(workspace: URI, storageService: IStorageService): boolean { if (!workspace) { return false; } diff --git a/src/vs/workbench/contrib/chat/common/chatUri.ts b/src/vs/workbench/contrib/chat/common/chatUri.ts index 596cf2606ca..792a10caa5a 100644 --- a/src/vs/workbench/contrib/chat/common/chatUri.ts +++ b/src/vs/workbench/contrib/chat/common/chatUri.ts @@ -28,6 +28,10 @@ export namespace LocalChatSessionUri { return parsed?.chatSessionType === localChatSessionType ? parsed.sessionId : undefined; } + export function isLocalSession(resource: URI): boolean { + return !!parseLocalSessionId(resource); + } + function parse(resource: URI): ChatSessionIdentifier | undefined { if (resource.scheme !== scheme) { return undefined; diff --git a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts index c08e11d1651..a3358e2782d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts @@ -30,7 +30,7 @@ class MockChatService implements IChatService { edits2Enabled: boolean = false; _serviceBrand: undefined; editingSessions = []; - transferredSessionData = undefined; + transferredSessionResource = undefined; readonly onDidSubmitRequest = Event.None; private sessions = new Map(); @@ -144,7 +144,7 @@ class MockChatService implements IChatService { notifyUserAction(_event: any): void { } - transferChatSession(): void { } + async transferChatSession(): Promise { } setChatSessionTitle(): void { } diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 7ae67169f9e..e8b59b9fbbe 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -24,6 +24,7 @@ import { ILogService, NullLogService } from '../../../../../platform/log/common/ import { IStorageService } from '../../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; +import { IUserDataProfilesService, toUserDataProfile } from '../../../../../platform/userDataProfile/common/userDataProfile.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; @@ -158,6 +159,7 @@ suite('ChatService', () => { ))); instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IUserDataProfilesService, { defaultProfile: toUserDataProfile('default', 'Default', URI.file('/test/userdata'), URI.file('/test/cache')) }); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); diff --git a/src/vs/workbench/contrib/chat/test/common/chatSessionStore.test.ts b/src/vs/workbench/contrib/chat/test/common/chatSessionStore.test.ts new file mode 100644 index 00000000000..f347bfc1604 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatSessionStore.test.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; +import { IStorageService } from '../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; +import { IUserDataProfilesService, toUserDataProfile } from '../../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IWorkspaceContextService, WorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js'; +import { TestWorkspace, Workspace } from '../../../../../platform/workspace/test/common/testWorkspace.js'; +import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; +import { InMemoryTestFileService, TestContextService, TestLifecycleService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { ChatModel } from '../../common/chatModel.js'; +import { ChatSessionStore, IChatTransfer } from '../../common/chatSessionStore.js'; +import { LocalChatSessionUri } from '../../common/chatUri.js'; +import { MockChatModel } from './mockChatModel.js'; + +function createMockChatModel(sessionResource: URI, options?: { customTitle?: string }): ChatModel { + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (!sessionId) { + throw new Error('createMockChatModel requires a local session URI'); + } + const model = new MockChatModel(sessionResource); + model.sessionId = sessionId; + if (options?.customTitle) { + model.customTitle = options.customTitle; + } + // Cast to ChatModel - the mock implements enough of the interface for testing + return model as unknown as ChatModel; +} + +suite('ChatSessionStore', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + + function createChatSessionStore(isEmptyWindow: boolean = false): ChatSessionStore { + const workspace = isEmptyWindow ? new Workspace('empty-window-id', []) : TestWorkspace; + instantiationService.stub(IWorkspaceContextService, new TestContextService(workspace)); + return testDisposables.add(instantiationService.createInstance(ChatSessionStore)); + } + + setup(() => { + instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection())); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, NullLogService); + instantiationService.stub(ITelemetryService, NullTelemetryService); + instantiationService.stub(IFileService, testDisposables.add(new InMemoryTestFileService())); + instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/workspaceStorage') }); + instantiationService.stub(ILifecycleService, testDisposables.add(new TestLifecycleService())); + instantiationService.stub(IUserDataProfilesService, { defaultProfile: toUserDataProfile('default', 'Default', URI.file('/test/userdata'), URI.file('/test/cache')) }); + }); + + test('hasSessions returns false when no sessions exist', () => { + const store = createChatSessionStore(); + + assert.strictEqual(store.hasSessions(), false); + }); + + test('getIndex returns empty index initially', async () => { + const store = createChatSessionStore(); + + const index = await store.getIndex(); + assert.deepStrictEqual(index, {}); + }); + + test('getChatStorageFolder returns correct path for workspace', () => { + const store = createChatSessionStore(false); + + const storageFolder = store.getChatStorageFolder(); + assert.ok(storageFolder.path.includes('workspaceStorage')); + assert.ok(storageFolder.path.includes('chatSessions')); + }); + + test('getChatStorageFolder returns correct path for empty window', () => { + const store = createChatSessionStore(true); + + const storageFolder = store.getChatStorageFolder(); + assert.ok(storageFolder.path.includes('emptyWindowChatSessions')); + }); + + test('isSessionEmpty returns true for non-existent session', () => { + const store = createChatSessionStore(); + + assert.strictEqual(store.isSessionEmpty('non-existent-session'), true); + }); + + test('readSession returns undefined for non-existent session', async () => { + const store = createChatSessionStore(); + + const session = await store.readSession('non-existent-session'); + assert.strictEqual(session, undefined); + }); + + test('deleteSession handles non-existent session gracefully', async () => { + const store = createChatSessionStore(); + + // Should not throw + await store.deleteSession('non-existent-session'); + + assert.strictEqual(store.hasSessions(), false); + }); + + test('storeSessions persists session to index', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + + await store.storeSessions([model]); + + assert.strictEqual(store.hasSessions(), true); + const index = await store.getIndex(); + assert.ok(index['session-1']); + assert.strictEqual(index['session-1'].sessionId, 'session-1'); + }); + + test('storeSessions persists custom title', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'), { customTitle: 'My Custom Title' })); + + await store.storeSessions([model]); + + const index = await store.getIndex(); + assert.strictEqual(index['session-1'].title, 'My Custom Title'); + }); + + test('readSession returns stored session data', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + + await store.storeSessions([model]); + const session = await store.readSession('session-1'); + + assert.ok(session); + assert.strictEqual(session.sessionId, 'session-1'); + }); + + test('deleteSession removes session from index', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + + await store.storeSessions([model]); + assert.strictEqual(store.hasSessions(), true); + + await store.deleteSession('session-1'); + + assert.strictEqual(store.hasSessions(), false); + const index = await store.getIndex(); + assert.strictEqual(index['session-1'], undefined); + }); + + test('clearAllSessions removes all sessions', async () => { + const store = createChatSessionStore(); + const model1 = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + const model2 = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-2'))); + + await store.storeSessions([model1, model2]); + assert.strictEqual(Object.keys(await store.getIndex()).length, 2); + + await store.clearAllSessions(); + + const index = await store.getIndex(); + assert.deepStrictEqual(index, {}); + }); + + test('setSessionTitle updates existing session title', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'), { customTitle: 'Original Title' })); + + await store.storeSessions([model]); + await store.setSessionTitle('session-1', 'New Title'); + + const index = await store.getIndex(); + assert.strictEqual(index['session-1'].title, 'New Title'); + }); + + test('setSessionTitle does nothing for non-existent session', async () => { + const store = createChatSessionStore(); + + // Should not throw + await store.setSessionTitle('non-existent', 'Title'); + + const index = await store.getIndex(); + assert.strictEqual(index['non-existent'], undefined); + }); + + test('multiple stores can be created with different workspaces', async () => { + const store1 = createChatSessionStore(false); + const store2 = createChatSessionStore(true); + + const folder1 = store1.getChatStorageFolder(); + const folder2 = store2.getChatStorageFolder(); + + assert.notStrictEqual(folder1.toString(), folder2.toString()); + }); + + suite('transferred sessions', () => { + function createSingleFolderWorkspace(folderUri: URI): Workspace { + const folder = new WorkspaceFolder({ uri: folderUri, index: 0, name: 'test' }); + return new Workspace('single-folder-id', [folder]); + } + + function createChatSessionStoreWithSingleFolder(folderUri: URI): ChatSessionStore { + instantiationService.stub(IWorkspaceContextService, new TestContextService(createSingleFolderWorkspace(folderUri))); + return testDisposables.add(instantiationService.createInstance(ChatSessionStore)); + } + + function createTransferData(toWorkspace: URI, sessionResource: URI, timestampInMilliseconds?: number): IChatTransfer { + return { + toWorkspace, + sessionResource, + timestampInMilliseconds: timestampInMilliseconds ?? Date.now(), + }; + } + + test('getTransferredSessionData returns undefined for empty window', () => { + const store = createChatSessionStore(true); // empty window + + const result = store.getTransferredSessionData(); + + assert.strictEqual(result, undefined); + }); + + test('getTransferredSessionData returns undefined when no transfer exists', () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + + const result = store.getTransferredSessionData(); + + assert.strictEqual(result, undefined); + }); + + test('storeTransferSession stores and retrieves transfer data', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + const transferData = createTransferData(folderUri, sessionResource); + await store.storeTransferSession(transferData, model); + + const result = store.getTransferredSessionData(); + assert.ok(result); + assert.strictEqual(result.toString(), sessionResource.toString()); + }); + + test('readTransferredSession returns session data', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + const transferData = createTransferData(folderUri, sessionResource); + await store.storeTransferSession(transferData, model); + + const sessionData = await store.readTransferredSession(sessionResource); + assert.ok(sessionData); + assert.strictEqual(sessionData.sessionId, 'transfer-session'); + }); + + test('readTransferredSession cleans up after reading', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + const transferData = createTransferData(folderUri, sessionResource); + await store.storeTransferSession(transferData, model); + + // Read the session + await store.readTransferredSession(sessionResource); + + // Transfer should be cleaned up + const result = store.getTransferredSessionData(); + assert.strictEqual(result, undefined); + }); + + test('getTransferredSessionData returns undefined for expired transfer', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + // Create transfer with timestamp 10 minutes in the past (expired) + const expiredTimestamp = Date.now() - (10 * 60 * 1000); + const transferData = createTransferData(folderUri, sessionResource, expiredTimestamp); + await store.storeTransferSession(transferData, model); + + const result = store.getTransferredSessionData(); + assert.strictEqual(result, undefined); + }); + + test('expired transfer cleans up index and file', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + // Create transfer with timestamp 100 minutes in the past (expired) + const expiredTimestamp = Date.now() - (100 * 60 * 1000); + const transferData = createTransferData(folderUri, sessionResource, expiredTimestamp); + await store.storeTransferSession(transferData, model); + + // Assert cleaned up + const data = store.getTransferredSessionData(); + assert.strictEqual(data, undefined); + }); + + test('readTransferredSession returns undefined for invalid session resource', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + + // Use a non-local session URI + const invalidResource = URI.parse('file:///invalid/session'); + + const result = await store.readTransferredSession(invalidResource); + assert.strictEqual(result, undefined); + }); + + test('storeTransferSession deletes preexisting transferred session file', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const fileService = instantiationService.get(IFileService); + + // Store first session + const session1Resource = LocalChatSessionUri.forSession('transfer-session-1'); + const model1 = testDisposables.add(createMockChatModel(session1Resource)); + const transferData1 = createTransferData(folderUri, session1Resource); + await store.storeTransferSession(transferData1, model1); + + // Verify first session file exists + const userDataProfile = instantiationService.get(IUserDataProfilesService).defaultProfile; + const storageLocation1 = URI.joinPath( + userDataProfile.globalStorageHome, + 'transferredChatSessions', + 'transfer-session-1.json' + ); + const exists1 = await fileService.exists(storageLocation1); + assert.strictEqual(exists1, true, 'First session file should exist'); + + // Store second session for the same workspace + const session2Resource = LocalChatSessionUri.forSession('transfer-session-2'); + const model2 = testDisposables.add(createMockChatModel(session2Resource)); + const transferData2 = createTransferData(folderUri, session2Resource); + await store.storeTransferSession(transferData2, model2); + + // Verify first session file is deleted + const exists1After = await fileService.exists(storageLocation1); + assert.strictEqual(exists1After, false, 'First session file should be deleted'); + + // Verify second session file exists + const storageLocation2 = URI.joinPath( + userDataProfile.globalStorageHome, + 'transferredChatSessions', + 'transfer-session-2.json' + ); + const exists2 = await fileService.exists(storageLocation2); + assert.strictEqual(exists2, true, 'Second session file should exist'); + + // Verify only the second session is retrievable + const result = store.getTransferredSessionData(); + assert.ok(result); + assert.strictEqual(result.toString(), session2Resource.toString()); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatModel.ts b/src/vs/workbench/contrib/chat/test/common/mockChatModel.ts index 3ffac4bc092..851ad51d5c5 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatModel.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatModel.ts @@ -14,12 +14,16 @@ import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatModel extends Disposable implements IChatModel { readonly onDidDispose = this._register(new Emitter()).event; readonly onDidChange = this._register(new Emitter()).event; - readonly sessionId = ''; + sessionId = ''; readonly timestamp = 0; readonly timing = { startTime: 0 }; readonly initialLocation = ChatAgentLocation.Chat; readonly title = ''; readonly hasCustomTitle = false; + customTitle: string | undefined; + lastMessageDate = Date.now(); + creationDate = Date.now(); + requests: IChatRequestModel[] = []; readonly requestInProgress = observableValue('requestInProgress', false); readonly requestNeedsInput = observableValue('requestNeedsInput', undefined); readonly inputPlaceholder = undefined; @@ -66,8 +70,8 @@ export class MockChatModel extends Disposable implements IChatModel { version: 3, sessionId: this.sessionId, creationDate: this.timestamp, - lastMessageDate: this.timestamp, - customTitle: undefined, + lastMessageDate: this.lastMessageDate, + customTitle: this.customTitle, initialLocation: this.initialLocation, requests: [], responderUsername: '', diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 3a56512be9f..ae582b3b4b7 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -10,7 +10,7 @@ import { IObservable, observableValue } from '../../../../../base/common/observa import { URI } from '../../../../../base/common/uri.js'; import { IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../common/chatModel.js'; import { IParsedChatRequest } from '../../common/chatParserTypes.js'; -import { IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatTransferredSessionData, IChatUserActionEvent } from '../../common/chatService.js'; +import { IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatService implements IChatService { @@ -19,7 +19,7 @@ export class MockChatService implements IChatService { edits2Enabled: boolean = false; _serviceBrand: undefined; editingSessions = []; - transferredSessionData: IChatTransferredSessionData | undefined; + transferredSessionResource: URI | undefined; readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }> = Event.None; private sessions = new ResourceMap(); @@ -104,7 +104,7 @@ export class MockChatService implements IChatService { } readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = undefined!; - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { + async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 57ba23f9975..577232d67e5 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -7,7 +7,7 @@ import { IContextMenuDelegate } from '../../../base/browser/contextmenu.js'; import { IDimension } from '../../../base/browser/dom.js'; import { Direction, IViewSize } from '../../../base/browser/ui/grid/grid.js'; import { mainWindow } from '../../../base/browser/window.js'; -import { DeferredPromise, timeout } from '../../../base/common/async.js'; +import { timeout } from '../../../base/common/async.js'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Codicon } from '../../../base/common/codicons.js'; @@ -26,7 +26,6 @@ import { assertReturnsDefined, upcast } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { ICodeEditor } from '../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js'; -import { IMarkdownRendererService, MarkdownRendererService } from '../../../platform/markdown/browser/markdownRenderer.js'; import { Position as EditorPosition, IPosition } from '../../../editor/common/core/position.js'; import { Range } from '../../../editor/common/core/range.js'; import { Selection } from '../../../editor/common/core/selection.js'; @@ -83,6 +82,7 @@ import { ILabelService } from '../../../platform/label/common/label.js'; import { ILayoutOffsetInfo } from '../../../platform/layout/browser/layoutService.js'; import { IListService } from '../../../platform/list/browser/listService.js'; import { ILoggerService, ILogService, NullLogService } from '../../../platform/log/common/log.js'; +import { IMarkdownRendererService, MarkdownRendererService } from '../../../platform/markdown/browser/markdownRenderer.js'; import { IMarkerService } from '../../../platform/markers/common/markers.js'; import { INotificationService } from '../../../platform/notification/common/notification.js'; import { TestNotificationService } from '../../../platform/notification/test/common/testNotificationService.js'; @@ -161,7 +161,7 @@ import { IHostService } from '../../services/host/browser/host.js'; import { LabelService } from '../../services/label/common/labelService.js'; import { ILanguageDetectionService } from '../../services/languageDetection/common/languageDetectionWorkerService.js'; import { IWorkbenchLayoutService, PanelAlignment, Position as PartPosition, Parts } from '../../services/layout/browser/layoutService.js'; -import { BeforeShutdownErrorEvent, ILifecycleService, InternalBeforeShutdownEvent, IWillShutdownEventJoiner, LifecyclePhase, ShutdownReason, StartupKind, WillShutdownEvent } from '../../services/lifecycle/common/lifecycle.js'; +import { ILifecycleService, InternalBeforeShutdownEvent, IWillShutdownEventJoiner, ShutdownReason, WillShutdownEvent } from '../../services/lifecycle/common/lifecycle.js'; import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js'; import { IPathService } from '../../services/path/common/pathService.js'; import { QuickInputService } from '../../services/quickinput/browser/quickInputService.js'; @@ -185,10 +185,10 @@ import { InMemoryWorkingCopyBackupService } from '../../services/workingCopy/com import { IWorkingCopyEditorService, WorkingCopyEditorService } from '../../services/workingCopy/common/workingCopyEditorService.js'; import { IWorkingCopyFileService, WorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js'; import { IWorkingCopyService, WorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js'; -import { TestChatEntitlementService, TestContextService, TestExtensionService, TestFileService, TestHistoryService, TestLoggerService, TestMarkerService, TestProductService, TestStorageService, TestTextResourcePropertiesService, TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from '../common/workbenchTestServices.js'; +import { TestChatEntitlementService, TestContextService, TestExtensionService, TestFileService, TestHistoryService, TestLifecycleService, TestLoggerService, TestMarkerService, TestProductService, TestStorageService, TestTextResourcePropertiesService, TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from '../common/workbenchTestServices.js'; // Backcompat export -export { TestFileService }; +export { TestFileService, TestLifecycleService }; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -1187,88 +1187,6 @@ export class InMemoryTestWorkingCopyBackupService extends BrowserWorkingCopyBack } } -export class TestLifecycleService extends Disposable implements ILifecycleService { - - declare readonly _serviceBrand: undefined; - - usePhases = false; - _phase!: LifecyclePhase; - get phase(): LifecyclePhase { return this._phase; } - set phase(value: LifecyclePhase) { - this._phase = value; - if (value === LifecyclePhase.Starting) { - this.whenStarted.complete(); - } else if (value === LifecyclePhase.Ready) { - this.whenReady.complete(); - } else if (value === LifecyclePhase.Restored) { - this.whenRestored.complete(); - } else if (value === LifecyclePhase.Eventually) { - this.whenEventually.complete(); - } - } - - private readonly whenStarted = new DeferredPromise(); - private readonly whenReady = new DeferredPromise(); - private readonly whenRestored = new DeferredPromise(); - private readonly whenEventually = new DeferredPromise(); - async when(phase: LifecyclePhase): Promise { - if (!this.usePhases) { - return; - } - if (phase === LifecyclePhase.Starting) { - await this.whenStarted.p; - } else if (phase === LifecyclePhase.Ready) { - await this.whenReady.p; - } else if (phase === LifecyclePhase.Restored) { - await this.whenRestored.p; - } else if (phase === LifecyclePhase.Eventually) { - await this.whenEventually.p; - } - } - - startupKind!: StartupKind; - willShutdown = false; - - private readonly _onBeforeShutdown = this._register(new Emitter()); - get onBeforeShutdown(): Event { return this._onBeforeShutdown.event; } - - private readonly _onBeforeShutdownError = this._register(new Emitter()); - get onBeforeShutdownError(): Event { return this._onBeforeShutdownError.event; } - - private readonly _onShutdownVeto = this._register(new Emitter()); - get onShutdownVeto(): Event { return this._onShutdownVeto.event; } - - private readonly _onWillShutdown = this._register(new Emitter()); - get onWillShutdown(): Event { return this._onWillShutdown.event; } - - private readonly _onDidShutdown = this._register(new Emitter()); - get onDidShutdown(): Event { return this._onDidShutdown.event; } - - shutdownJoiners: Promise[] = []; - - fireShutdown(reason = ShutdownReason.QUIT): void { - this.shutdownJoiners = []; - - this._onWillShutdown.fire({ - join: p => { - this.shutdownJoiners.push(typeof p === 'function' ? p() : p); - }, - joiners: () => [], - force: () => { /* No-Op in tests */ }, - token: CancellationToken.None, - reason - }); - } - - fireBeforeShutdown(event: InternalBeforeShutdownEvent): void { this._onBeforeShutdown.fire(event); } - - fireWillShutdown(event: WillShutdownEvent): void { this._onWillShutdown.fire(event); } - - async shutdown(): Promise { - this.fireShutdown(); - } -} - export class TestBeforeShutdownEvent implements InternalBeforeShutdownEvent { value: boolean | Promise | undefined; diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index c5c6e145e08..0000856e22c 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from '../../../base/common/async.js'; +import { DeferredPromise, timeout } from '../../../base/common/async.js'; import { bufferToStream, readableToBuffer, VSBuffer, VSBufferReadable } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; @@ -36,6 +36,7 @@ import { ChatEntitlement, IChatEntitlementService } from '../../services/chat/co import { NullExtensionService } from '../../services/extensions/common/extensions.js'; import { IAutoSaveConfiguration, IAutoSaveMode, IFilesConfigurationService } from '../../services/filesConfiguration/common/filesConfigurationService.js'; import { IHistoryService } from '../../services/history/common/history.js'; +import { BeforeShutdownErrorEvent, ILifecycleService, InternalBeforeShutdownEvent, LifecyclePhase, ShutdownReason, StartupKind, WillShutdownEvent } from '../../services/lifecycle/common/lifecycle.js'; import { IResourceEncoding } from '../../services/textfile/common/textfiles.js'; import { IUserDataProfileService } from '../../services/userDataProfile/common/userDataProfile.js'; import { IStoredFileWorkingCopySaveEvent } from '../../services/workingCopy/common/storedFileWorkingCopy.js'; @@ -698,7 +699,7 @@ export class TestFileService implements IFileService { */ export class InMemoryTestFileService extends TestFileService { - private files = new Map(); + private files = new ResourceMap(); override clearTracking(): void { super.clearTracking(); @@ -714,7 +715,7 @@ export class InMemoryTestFileService extends TestFileService { this.readOperations.push({ resource }); // Check if we have content in our in-memory store - const content = this.files.get(resource.toString()); + const content = this.files.get(resource); if (content) { return { ...createFileStat(resource, this.readonly), @@ -743,11 +744,25 @@ export class InMemoryTestFileService extends TestFileService { } // Store in memory and track - this.files.set(resource.toString(), content); + this.files.set(resource, content); this.writeOperations.push({ resource, content: content.toString() }); return createFileStat(resource, this.readonly); } + + override async del(resource: URI, _options?: { useTrash?: boolean; recursive?: boolean }): Promise { + this.files.delete(resource); + this.notExistsSet.set(resource, true); + } + + override async exists(resource: URI): Promise { + const inMemory = this.files.has(resource); + if (inMemory) { + return true; + } + + return super.exists(resource); + } } export class TestChatEntitlementService implements IChatEntitlementService { @@ -779,3 +794,84 @@ export class TestChatEntitlementService implements IChatEntitlementService { readonly anonymousObs = observableValue({}, false); } +export class TestLifecycleService extends Disposable implements ILifecycleService { + + declare readonly _serviceBrand: undefined; + + usePhases = false; + _phase!: LifecyclePhase; + get phase(): LifecyclePhase { return this._phase; } + set phase(value: LifecyclePhase) { + this._phase = value; + if (value === LifecyclePhase.Starting) { + this.whenStarted.complete(); + } else if (value === LifecyclePhase.Ready) { + this.whenReady.complete(); + } else if (value === LifecyclePhase.Restored) { + this.whenRestored.complete(); + } else if (value === LifecyclePhase.Eventually) { + this.whenEventually.complete(); + } + } + + private readonly whenStarted = new DeferredPromise(); + private readonly whenReady = new DeferredPromise(); + private readonly whenRestored = new DeferredPromise(); + private readonly whenEventually = new DeferredPromise(); + async when(phase: LifecyclePhase): Promise { + if (!this.usePhases) { + return; + } + if (phase === LifecyclePhase.Starting) { + await this.whenStarted.p; + } else if (phase === LifecyclePhase.Ready) { + await this.whenReady.p; + } else if (phase === LifecyclePhase.Restored) { + await this.whenRestored.p; + } else if (phase === LifecyclePhase.Eventually) { + await this.whenEventually.p; + } + } + + startupKind!: StartupKind; + willShutdown = false; + + private readonly _onBeforeShutdown = this._register(new Emitter()); + get onBeforeShutdown(): Event { return this._onBeforeShutdown.event; } + + private readonly _onBeforeShutdownError = this._register(new Emitter()); + get onBeforeShutdownError(): Event { return this._onBeforeShutdownError.event; } + + private readonly _onShutdownVeto = this._register(new Emitter()); + get onShutdownVeto(): Event { return this._onShutdownVeto.event; } + + private readonly _onWillShutdown = this._register(new Emitter()); + get onWillShutdown(): Event { return this._onWillShutdown.event; } + + private readonly _onDidShutdown = this._register(new Emitter()); + get onDidShutdown(): Event { return this._onDidShutdown.event; } + + shutdownJoiners: Promise[] = []; + + fireShutdown(reason = ShutdownReason.QUIT): void { + this.shutdownJoiners = []; + + this._onWillShutdown.fire({ + join: p => { + this.shutdownJoiners.push(typeof p === 'function' ? p() : p); + }, + joiners: () => [], + force: () => { /* No-Op in tests */ }, + token: CancellationToken.None, + reason + }); + } + + fireBeforeShutdown(event: InternalBeforeShutdownEvent): void { this._onBeforeShutdown.fire(event); } + + fireWillShutdown(event: WillShutdownEvent): void { this._onWillShutdown.fire(event); } + + async shutdown(): Promise { + this.fireShutdown(); + } +} diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index d6c4c7b5296..19eae8d7f37 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -6,6 +6,6 @@ declare module 'vscode' { export namespace interactive { - export function transferActiveChat(toWorkspace: Uri): void; + export function transferActiveChat(toWorkspace: Uri): Thenable; } } From ec6406bfc5ee05f513c4236f1ce0cd990774e8f9 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 20 Dec 2025 18:21:59 -0800 Subject: [PATCH 15/25] Let built-in chat participants share metadata (#284636) So we can find the response ID in order to restore Turns Fix #272987 --- src/vs/workbench/api/common/extHostChatAgents2.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 047976cf458..74b59a49594 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -721,7 +721,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS for (const h of context.history) { const ehResult = typeConvert.ChatAgentResult.to(h.result); - const result: vscode.ChatResult = agentId === h.request.agentId ? + const result: vscode.ChatResult = agentId === h.request.agentId || (isBuiltinParticipant(h.request.agentId) && isBuiltinParticipant(agentId)) ? ehResult : { ...ehResult, metadata: undefined }; @@ -1122,3 +1122,7 @@ function raceCancellationWithTimeout(cancelWait: number, promise: Promise, promise.then(resolve, reject).finally(() => ref.dispose()); }); } + +function isBuiltinParticipant(agentId: string): boolean { + return agentId.startsWith('github.copilot'); +} From 86d7cec11fbafd9d43438c9389e3c41270007c8d Mon Sep 17 00:00:00 2001 From: Takashi Tamura Date: Sun, 21 Dec 2025 12:28:41 +0900 Subject: [PATCH 16/25] fix(terminalChatAgentTools): include uppercase -I in sed in-place option detection --- .../common/terminalChatAgentToolsConfiguration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index c0c9a6c9b98..68f4bc6390d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -271,7 +271,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Sun, 21 Dec 2025 14:13:11 +0900 Subject: [PATCH 17/25] test(runInTerminalTool): add test case for sed in-place option with uppercase -I --- .../test/electron-browser/runInTerminalTool.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 1aeb04cf342..5ecba8aa106 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -304,6 +304,7 @@ suite('RunInTerminalTool', () => { 'rg --hostname-bin hostname pattern .', 'sed -i "s/foo/bar/g" file.txt', 'sed -i.bak "s/foo/bar/" file.txt', + 'sed -Ibak "s/foo/bar/" file.txt', 'sed --in-place "s/foo/bar/" file.txt', 'sed -e "s/a/b/" file.txt', 'sed -f script.sed file.txt', From b5ef2e0fc00c1f4c36be41fe32f50a779bd34137 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:52:26 +0800 Subject: [PATCH 18/25] make selections pinnable as implicit context (#284657) * make selections pinnable as implicit context * extra edit not needed * reduce duplicated code --- .../attachments/implicitContextAttachment.ts | 20 +++++++ .../contrib/chat/browser/chatInputPart.ts | 52 +++++++++++++------ .../contrib/chat/browser/media/chat.css | 4 ++ 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 244809b8019..e94d7ebde25 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -84,6 +84,15 @@ export class ImplicitContextAttachmentWidget extends Disposable { } this.attachment.enabled = false; })); + } else { + const pinButtonMsg = localize('pinSelection', "Pin selection"); + const pinButton = this.renderDisposables.add(new Button(this.domNode, { supportIcons: true, title: pinButtonMsg })); + pinButton.icon = Codicon.pinned; + this.renderDisposables.add(pinButton.onDidClick(async (e) => { + e.stopPropagation(); + e.preventDefault(); + await this.pinSelection(); + })); } if (!this.attachment.enabled && this.attachment.isSelection) { @@ -209,4 +218,15 @@ export class ImplicitContextAttachmentWidget extends Disposable { } this.widgetRef()?.focusInput(); } + private async pinSelection(): Promise { + if (!this.attachment.value || !this.attachment.isSelection) { + return; + } + + if (!URI.isUri(this.attachment.value) && !isStringImplicitContextValue(this.attachment.value)) { + const location = this.attachment.value; + this.attachmentModel.addFile(location.uri, location.range); + } + this.widgetRef()?.focusInput(); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d37d3c361c7..f915d76de35 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -38,6 +38,7 @@ import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/c import { EditorOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; import { IDimension } from '../../../../editor/common/core/2d/dimension.js'; import { IPosition } from '../../../../editor/common/core/position.js'; +import { IRange, Range } from '../../../../editor/common/core/range.js'; import { isLocation } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -1859,18 +1860,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (isSuggestedEnabled && implicitValue) { const targetUri: URI | undefined = this.implicitContext.uri; - - const currentlyAttached = attachments.some(([, attachment]) => { - let uri: URI | undefined; - if (URI.isUri(attachment.value)) { - uri = attachment.value; - } else if (isStringVariableEntry(attachment)) { - uri = attachment.uri; - } - return uri && isEqual(uri, targetUri); - }); - - const shouldShowImplicit = !isLocation(implicitValue) ? !currentlyAttached : implicitValue.range; + const targetRange = isLocation(implicitValue) ? implicitValue.range : undefined; + const currentlyAttached = this.isAttachmentAlreadyAttached(targetUri, targetRange, attachments.map(([, a]) => a)); + const shouldShowImplicit = !currentlyAttached; if (shouldShowImplicit) { const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, () => this._widget, this.implicitContext, this._contextResourceLabels, this._attachmentModel)); container.appendChild(implicitPart.domNode); @@ -1896,18 +1888,44 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return true; } - // TODO @justschen: merge this with above showing implicit logic const isUri = URI.isUri(implicit); if (isUri || isLocation(implicit)) { const targetUri = isUri ? implicit : implicit.uri; - const attachments = [...this._attachmentModel.attachments.entries()]; - const currentlyAttached = attachments.some(([, a]) => URI.isUri(a.value) && isEqual(a.value, targetUri)); - const shouldShowImplicit = isUri ? !currentlyAttached : implicit.range; - return !!shouldShowImplicit; + const targetRange = isLocation(implicit) ? implicit.range : undefined; + const attachments = [...this._attachmentModel.attachments.values()]; + const currentlyAttached = this.isAttachmentAlreadyAttached(targetUri, targetRange, attachments); + return !currentlyAttached; } return false; } + private isAttachmentAlreadyAttached(targetUri: URI | undefined, targetRange: IRange | undefined, attachments: IChatRequestVariableEntry[]): boolean { + return attachments.some((attachment) => { + let uri: URI | undefined; + let range: IRange | undefined; + + if (URI.isUri(attachment.value)) { + uri = attachment.value; + } else if (isLocation(attachment.value)) { + uri = attachment.value.uri; + range = attachment.value.range; + } else if (isStringVariableEntry(attachment)) { + uri = attachment.uri; + } + + if (!uri || !isEqual(uri, targetUri)) { + return false; + } + + // check if the exact range is already attached + if (targetRange) { + return range && Range.equalsRange(range, targetRange); + } + + return true; + }); + } + private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) { // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) if (dom.isKeyboardEvent(e)) { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 52d32756c40..a186308927f 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -1593,6 +1593,10 @@ have to be updated for changes to the rules above, or to support more deeply nes width: fit-content; } +.action-item.chat-attachment-button .action-label { + padding: 0 4px; +} + .interactive-session .interactive-list .chat-attached-context .chat-attached-context-attachment { font-family: var(--vscode-chat-font-family, inherit); font-size: var(--vscode-chat-font-size-body-xs); From de33fdc814d6da67fe2e398239ab89f00281b4e8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 Dec 2025 13:02:34 -0800 Subject: [PATCH 19/25] Move resize dims overlay into terminalContrib Part of #284046 --- .../terminal/browser/media/terminal.css | 28 -------------- .../terminal/browser/terminalInstance.ts | 13 ------- .../contrib/terminal/terminal.all.ts | 1 + .../media/terminalResizeDimensionsOverlay.css | 32 ++++++++++++++++ ...al.resizeDimensionsOverlay.contribution.ts | 37 +++++++++++++++++++ .../terminalResizeDimensionsOverlay.ts | 15 +++++--- 6 files changed, 79 insertions(+), 47 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/media/terminalResizeDimensionsOverlay.css create mode 100644 src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.ts rename src/vs/workbench/contrib/{terminal => terminalContrib/resizeDimensionsOverlay}/browser/terminalResizeDimensionsOverlay.ts (78%) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 7684bbcbb26..a9b8428bd8e 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -16,34 +16,6 @@ z-index: 0; } -.monaco-workbench .terminal-resize-overlay { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - padding: 4px 10px; - border-radius: 4px; - background-color: var(--vscode-editorWidget-background); - color: var(--vscode-editorWidget-foreground); - border: 1px solid var(--vscode-editorWidget-border); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); - pointer-events: none; - opacity: 0; - transition: opacity 80ms ease-out; - z-index: 35; - font-size: 11px; -} - -.monaco-workbench.hc-black .terminal-resize-overlay, -.monaco-workbench.hc-light .terminal-resize-overlay { - box-shadow: none; - border-color: var(--vscode-contrastBorder); -} - -.monaco-workbench .terminal-resize-overlay.visible { - opacity: 1; -} - .terminal-command-decoration.hide { visibility: hidden; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 6ac7bfbbfa9..58e4b33c649 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -94,7 +94,6 @@ import { refreshShellIntegrationInfoStatus } from './terminalTooltip.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { PromptInputState } from '../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; import { hasKey, isNumber, isString } from '../../../../base/common/types.js'; -import { TerminalResizeDimensionsOverlay } from './terminalResizeDimensionsOverlay.js'; const enum Constants { /** @@ -203,7 +202,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _lineDataEventAddon: LineDataEventAddon | undefined; private readonly _scopedContextKeyService: IContextKeyService; private _resizeDebouncer?: TerminalResizeDebouncer; - private readonly _terminalResizeDimensionsOverlay: MutableDisposable = this._register(new MutableDisposable()); readonly capabilities = this._register(new TerminalCapabilityStoreMultiplexer()); readonly statusList: ITerminalStatusList; @@ -1182,17 +1180,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } this.updateConfig(); - // Initialize resize dimensions overlay - this.processReady.then(() => { - // Wait a second to avoid resize events during startup like when opening a terminal or - // when a terminal reconnects. Ideally we'd have an actual event to listen to here. - timeout(1000).then(() => { - if (!this._store.isDisposed) { - this._terminalResizeDimensionsOverlay.value = new TerminalResizeDimensionsOverlay(this._wrapperElement, xterm); - } - }); - }); - // If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal // panel was initialized. if (xterm.raw.options.disableStdin) { diff --git a/src/vs/workbench/contrib/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index 9cbb67bd019..060281d8a08 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -28,6 +28,7 @@ import '../terminalContrib/stickyScroll/browser/terminal.stickyScroll.contributi import '../terminalContrib/quickAccess/browser/terminal.quickAccess.contribution.js'; import '../terminalContrib/quickFix/browser/terminal.quickFix.contribution.js'; import '../terminalContrib/typeAhead/browser/terminal.typeAhead.contribution.js'; +import '../terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.js'; import '../terminalContrib/sendSequence/browser/terminal.sendSequence.contribution.js'; import '../terminalContrib/sendSignal/browser/terminal.sendSignal.contribution.js'; import '../terminalContrib/suggest/browser/terminal.suggest.contribution.js'; diff --git a/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/media/terminalResizeDimensionsOverlay.css b/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/media/terminalResizeDimensionsOverlay.css new file mode 100644 index 00000000000..e6dd5520aa7 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/media/terminalResizeDimensionsOverlay.css @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .terminal-resize-overlay { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + padding: 4px 10px; + border-radius: 4px; + background-color: var(--vscode-editorWidget-background); + color: var(--vscode-editorWidget-foreground); + border: 1px solid var(--vscode-editorWidget-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + pointer-events: none; + opacity: 0; + transition: opacity 80ms ease-out; + z-index: 35; + font-size: 11px; +} + +.monaco-workbench.hc-black .terminal-resize-overlay, +.monaco-workbench.hc-light .terminal-resize-overlay { + box-shadow: none; + border-color: var(--vscode-contrastBorder); +} + +.monaco-workbench .terminal-resize-overlay.visible { + opacity: 1; +} diff --git a/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.ts b/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.ts new file mode 100644 index 00000000000..665d52e06d5 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import { Disposable, MutableDisposable, type IDisposable } from '../../../../../base/common/lifecycle.js'; +import type { ITerminalContribution, IXtermTerminal } from '../../../terminal/browser/terminal.js'; +import { registerTerminalContribution, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; +import { timeout } from '../../../../../base/common/async.js'; +import { TerminalResizeDimensionsOverlay } from './terminalResizeDimensionsOverlay.js'; + +class TerminalResizeDimensionsOverlayContribution extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.resizeDimensionsOverlay'; + + private readonly _overlay: MutableDisposable = this._register(new MutableDisposable()); + + constructor( + private readonly _ctx: ITerminalContributionContext, + ) { + super(); + } + + xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + // Initialize resize dimensions overlay + this._ctx.processManager.ptyProcessReady.then(() => { + // Wait a second to avoid resize events during startup like when opening a terminal or + // when a terminal reconnects. Ideally we'd have an actual event to listen to here. + timeout(1000).then(() => { + if (!this._store.isDisposed) { + this._overlay.value = new TerminalResizeDimensionsOverlay(this._ctx.instance.domElement, xterm); + } + }); + }); + } +} +registerTerminalContribution(TerminalResizeDimensionsOverlayContribution.ID, TerminalResizeDimensionsOverlayContribution); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalResizeDimensionsOverlay.ts b/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/terminalResizeDimensionsOverlay.ts similarity index 78% rename from src/vs/workbench/contrib/terminal/browser/terminalResizeDimensionsOverlay.ts rename to src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/terminalResizeDimensionsOverlay.ts index 6419d00af88..4ecc329002c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalResizeDimensionsOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/resizeDimensionsOverlay/browser/terminalResizeDimensionsOverlay.ts @@ -3,10 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $ } from '../../../../base/browser/dom.js'; -import { disposableTimeout } from '../../../../base/common/async.js'; -import { Disposable, MutableDisposable, toDisposable, type IDisposable } from '../../../../base/common/lifecycle.js'; -import type { XtermTerminal } from './xterm/xtermTerminal.js'; + +import './media/terminalResizeDimensionsOverlay.css'; +import { $ } from '../../../../../base/browser/dom.js'; +import { disposableTimeout } from '../../../../../base/common/async.js'; +import { Disposable, MutableDisposable, toDisposable, type IDisposable } from '../../../../../base/common/lifecycle.js'; +import type { IXtermTerminal } from '../../../terminal/browser/terminal.js'; +import type { XtermTerminal } from '../../../terminal/browser/xterm/xtermTerminal.js'; const enum Constants { ResizeOverlayHideDelay = 500, @@ -20,11 +23,11 @@ export class TerminalResizeDimensionsOverlay extends Disposable { constructor( private readonly _container: HTMLElement, - xterm: XtermTerminal, + xterm: IXtermTerminal, ) { super(); - this._register(xterm.raw.onResize(dims => this._handleDimensionsChanged(dims))); + this._register((xterm as XtermTerminal).raw.onResize(dims => this._handleDimensionsChanged(dims))); this._register(toDisposable(() => { this._resizeOverlay?.remove(); this._resizeOverlay = undefined; From a4451f532b1053a1a4bf4b371ae7b7054db93d9b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 21 Dec 2025 17:12:12 -0800 Subject: [PATCH 20/25] Fix "add context" toolbar item slow to appear (#284706) We did the attachments layout before creating the toolbar, so its container would start hidden, then it wouldn't appear until some other random thing would trigger renderAttachedContext, generally the onDidFileIconThemeChange event which happened after some delay --- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f915d76de35..ee24501aaa4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1476,7 +1476,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.tryUpdateWidgetController(); - this.renderAttachedContext(); this._register(this._attachmentModel.onDidChange((e) => { if (e.added.length > 0) { this._indexOfLastAttachedContextDeletedWithKeyboard = -1; @@ -1759,6 +1758,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidChangeHeight.fire(); } })); + this.renderAttachedContext(); } public toggleChatInputOverlay(editing: boolean): void { From cf186fdab78fd5af42c16b35ae20f4d19d99ca98 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:46:55 +0000 Subject: [PATCH 21/25] Git - update git worktree inlint action (#284737) --- extensions/git/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 2c6eadd99dd..b8b97f4f116 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1128,6 +1128,7 @@ { "command": "git.repositories.openWorktreeInNewWindow", "title": "%command.openWorktreeInNewWindow2%", + "icon": "$(folder-opened)", "category": "Git", "enablement": "!operationInProgress" }, @@ -2116,7 +2117,7 @@ "when": "scmProvider == git && (scmArtifactGroupId == branches || scmArtifactGroupId == tags)" }, { - "command": "git.repositories.openWorktree", + "command": "git.repositories.openWorktreeInNewWindow", "group": "inline@1", "when": "scmProvider == git && scmArtifactGroupId == worktrees" }, From 045a6c2efcdf104845721e1b1e99c3a646715e72 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:38:06 +0000 Subject: [PATCH 22/25] SCM - fix issue with repository selection (#284748) --- src/vs/workbench/api/common/extHostSCM.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 57a6ef48497..a7c37795384 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -1125,6 +1125,9 @@ export class ExtHostSCM implements ExtHostSCMShape { $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise { this.logService.trace('ExtHostSCM#$setSelectedSourceControl', selectedSourceControlHandle); + if (this._selectedSourceControlHandle === selectedSourceControlHandle) { + return Promise.resolve(undefined); + } if (selectedSourceControlHandle !== undefined) { this._sourceControls.get(selectedSourceControlHandle)?.setSelectionState(true); From e78d8b50851b6cf3c427c2078b6238446f06beff Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Dec 2025 12:51:29 -0800 Subject: [PATCH 23/25] Reduce debt in ChatService (#284777) Fix #278996 --- .../contrib/chat/browser/chatEditorInput.ts | 5 +- .../contrib/chat/common/chatService.ts | 3 +- .../contrib/chat/common/chatServiceImpl.ts | 122 +++++------------- .../contrib/chat/common/chatSessionStore.ts | 10 ++ .../localAgentSessionsProvider.test.ts | 6 +- .../chat/test/common/mockChatService.ts | 6 +- .../chat/browser/terminalChatActions.ts | 10 +- 7 files changed, 50 insertions(+), 112 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts index ebc8545b839..540accfae31 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts @@ -96,8 +96,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler // Check if we already have a custom title for this session const hasExistingCustomTitle = this._sessionResource && ( - this.chatService.getSession(this._sessionResource)?.title || - this.chatService.getPersistedSessionTitle(this._sessionResource)?.trim() + this.chatService.getSessionTitle(this._sessionResource)?.trim() ); this.hasCustomTitle = Boolean(hasExistingCustomTitle); @@ -184,7 +183,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler } // If not in active registry, try persisted session data - const persistedTitle = this.chatService.getPersistedSessionTitle(this._sessionResource); + const persistedTitle = this.chatService.getSessionTitle(this._sessionResource); if (persistedTitle && persistedTitle.trim()) { // Only use non-empty persisted titles return persistedTitle; } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 21f972a109b..8abec4cf580 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -1024,8 +1024,7 @@ export interface IChatService { getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined; getOrRestoreSession(sessionResource: URI): Promise; - getPersistedSessionTitle(sessionResource: URI): string | undefined; - isPersistedSessionEmpty(sessionResource: URI): boolean; + getSessionTitle(sessionResource: URI): string | undefined; loadSessionFromContent(data: IExportableChatData | ISerializableChatData | URI): IChatModelReference | undefined; loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise; readonly editingSessions: IChatEditingSession[]; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index da263d91534..83c77342543 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -76,7 +76,6 @@ export class ChatService extends Disposable implements IChatService { private readonly _sessionModels: ChatModelStore; private readonly _pendingRequests = this._register(new DisposableResourceMap()); - private _persistedSessions: ISerializableChatsData; private _saveModelsEnabled = true; private _transferredSessionResource: URI | undefined; @@ -125,7 +124,7 @@ export class ChatService extends Disposable implements IChatService { } constructor( - @IStorageService storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, @ILogService private readonly logService: ILogService, @IExtensionService private readonly extensionService: IExtensionService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -160,20 +159,8 @@ export class ChatService extends Disposable implements IChatService { })); this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry); - - const sessionData = storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, ''); - if (sessionData) { - this._persistedSessions = this.deserializeChats(sessionData); - const countsForLog = Object.keys(this._persistedSessions).length; - if (countsForLog > 0) { - this.trace('constructor', `Restored ${countsForLog} persisted sessions`); - } - } else { - this._persistedSessions = {}; - } - this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore)); - this._chatSessionStore.migrateDataIfNeeded(() => this._persistedSessions); + this._chatSessionStore.migrateDataIfNeeded(() => this.migrateData()); const transferredData = this._chatSessionStore.getTransferredSessionData(); if (transferredData) { @@ -181,11 +168,7 @@ export class ChatService extends Disposable implements IChatService { this._transferredSessionResource = transferredData; } - // When using file storage, populate _persistedSessions with session metadata from the index - // This ensures that getPersistedSessionTitle() can find titles for inactive sessions - this.initializePersistedSessionsFromFileStorage().then(() => { - this.reviveSessionsWithEdits(); - }); + this.reviveSessionsWithEdits(); this._register(storageService.onWillSaveState(() => this.saveState())); @@ -205,6 +188,21 @@ export class ChatService extends Disposable implements IChatService { return this.chatAgentService.getContributedDefaultAgent(location) !== undefined; } + private migrateData(): ISerializableChatsData | undefined { + const sessionData = this.storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, ''); + if (sessionData) { + const persistedSessions = this.deserializeChats(sessionData); + const countsForLog = Object.keys(persistedSessions).length; + if (countsForLog > 0) { + this.info('migrateData', `Restored ${countsForLog} persisted sessions`); + } + + return persistedSessions; + } + + return; + } + private saveState(): void { if (!this._saveModelsEnabled) { return; @@ -264,6 +262,14 @@ export class ChatService extends Disposable implements IChatService { } } + private info(method: string, message?: string): void { + if (message) { + this.logService.info(`ChatService#${method}: ${message}`); + } else { + this.logService.info(`ChatService#${method}`); + } + } + private error(method: string, message: string): void { this.logService.error(`ChatService#${method} ${message}`); } @@ -304,7 +310,8 @@ export class ChatService extends Disposable implements IChatService { * todo@connor4312 This will be cleaned up with the globalization of edits. */ private async reviveSessionsWithEdits(): Promise { - await Promise.all(Object.values(this._persistedSessions).map(async session => { + const idx = await this._chatSessionStore.getIndex(); + await Promise.all(Object.values(idx).map(async session => { if (!session.hasPendingEdits) { return; } @@ -319,34 +326,6 @@ export class ChatService extends Disposable implements IChatService { })); } - private async initializePersistedSessionsFromFileStorage(): Promise { - - const index = await this._chatSessionStore.getIndex(); - const sessionIds = Object.keys(index); - - for (const sessionId of sessionIds) { - const metadata = index[sessionId]; - if (metadata && !this._persistedSessions[sessionId]) { - // Create a minimal session entry with the title information - // This allows getPersistedSessionTitle() to find the title without loading the full session - const minimalSession: ISerializableChatData = { - version: 3, - sessionId: sessionId, - customTitle: metadata.title, - creationDate: Date.now(), // Use current time as fallback - lastMessageDate: metadata.lastMessageDate, - initialLocation: metadata.initialLocation, - requests: [], // Empty requests array - this is just for title lookup - responderUsername: '', - responderAvatarIconUri: undefined, - hasPendingEdits: metadata.hasPendingEdits, - }; - - this._persistedSessions[sessionId] = minimalSession; - } - } - } - /** * Returns an array of chat details for all persisted chat sessions that have at least one request. * Chat sessions that have already been loaded into the chat view are excluded from the result. @@ -536,51 +515,16 @@ export class ChatService extends Disposable implements IChatService { return sessionRef; } - /** - * This is really just for migrating data from the edit session location to the panel. - */ - isPersistedSessionEmpty(sessionResource: URI): boolean { - const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (!sessionId) { - throw new Error(`Cannot restore non-local session ${sessionResource}`); - } - - const session = this._persistedSessions[sessionId]; - if (session) { - return session.requests.length === 0; - } - - return this._chatSessionStore.isSessionEmpty(sessionId); - } - - getPersistedSessionTitle(sessionResource: URI): string | undefined { + // There are some cases where this returns a real string. What happens if it doesn't? + // This had titles restored from the index, so just return titles from index instead, sync. + getSessionTitle(sessionResource: URI): string | undefined { const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); if (!sessionId) { return undefined; } - // First check the memory cache (_persistedSessions) - const session = this._persistedSessions[sessionId]; - if (session) { - const title = session.customTitle || ChatModel.getDefaultTitle(session.requests); - return title; - } - - // Try to read directly from file storage index - // This handles the case where getName() is called before initialization completes - // Access the internal synchronous index method via reflection - // This is a workaround for the timing issue where initialization hasn't completed - // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any - const internalGetIndex = (this._chatSessionStore as any).internalGetIndex; - if (typeof internalGetIndex === 'function') { - const indexData = internalGetIndex.call(this._chatSessionStore); - const metadata = indexData.entries[sessionId]; - if (metadata && metadata.title) { - return metadata.title; - } - } - - return undefined; + return this._sessionModels.get(sessionResource)?.title ?? + this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title; } loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModelReference | undefined { diff --git a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts index 47218ece5c1..5cf7af39024 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts @@ -419,6 +419,16 @@ export class ChatSessionStore extends Disposable { }); } + getMetadataForSessionSync(sessionResource: URI): IChatSessionEntryMetadata | undefined { + const index = this.internalGetIndex(); + return index.entries[this.getIndexKey(sessionResource)]; + } + + private getIndexKey(sessionResource: URI): string { + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + return sessionId ?? sessionResource.toString(); + } + logIndex(): void { const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined); this.logService.info('ChatSessionStore index: ', data); diff --git a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts index a3358e2782d..65d21245c4b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts @@ -92,7 +92,7 @@ class MockChatService implements IChatService { throw new Error('Method not implemented.'); } - getPersistedSessionTitle(_sessionResource: URI): string | undefined { + getSessionTitle(_sessionResource: URI): string | undefined { return undefined; } @@ -158,10 +158,6 @@ class MockChatService implements IChatService { logChatIndex(): void { } - isPersistedSessionEmpty(_sessionResource: URI): boolean { - return false; - } - activateDefaultAgent(_location: ChatAgentLocation): Promise { return Promise.resolve(); } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index ae582b3b4b7..d826d3e2461 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -49,7 +49,7 @@ export class MockChatService implements IChatService { async getOrRestoreSession(sessionResource: URI): Promise { throw new Error('Method not implemented.'); } - getPersistedSessionTitle(sessionResource: URI): string | undefined { + getSessionTitle(sessionResource: URI): string | undefined { throw new Error('Method not implemented.'); } loadSessionFromContent(data: ISerializableChatData): IChatModelReference | undefined { @@ -124,10 +124,6 @@ export class MockChatService implements IChatService { throw new Error('Method not implemented.'); } - isPersistedSessionEmpty(sessionResource: URI): boolean { - throw new Error('Method not implemented.'); - } - activateDefaultAgent(location: ChatAgentLocation): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 4f461a8473d..b07810ca9b2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -379,16 +379,10 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { const chatSessionId = terminalChatService.getChatSessionIdForInstance(instance); let chatSessionTitle: string | undefined; if (chatSessionId) { - const sessionUri = LocalChatSessionUri.forSession(chatSessionId); - // Try to get title from active session first, then fall back to persisted title - chatSessionTitle = chatService.getSession(sessionUri)?.title || chatService.getPersistedSessionTitle(sessionUri); - } - - let description: string | undefined; - if (chatSessionTitle) { - description = `${chatSessionTitle}`; + chatSessionTitle = chatService.getSessionTitle(LocalChatSessionUri.forSession(chatSessionId)); } + const description = chatSessionTitle; let detail: string | undefined; let tooltip: string | IMarkdownString | undefined; if (lastCommand) { From e90ece8f4d188e9a931f8d74c596700670b26e6f Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 22 Dec 2025 20:00:54 -0800 Subject: [PATCH 24/25] Some lint fixes (#284809) --- eslint.config.js | 8 -------- src/vs/workbench/contrib/chat/common/annotations.ts | 7 ++++--- src/vs/workbench/contrib/chat/common/chat.ts | 4 ++-- src/vs/workbench/contrib/chat/common/chatAgents.ts | 6 +++++- src/vs/workbench/contrib/chat/common/chatService.ts | 12 +++++++++++- .../workbench/contrib/chat/common/chatServiceImpl.ts | 8 ++++---- .../contrib/chat/common/codeBlockModelCollection.ts | 3 ++- .../contrib/chat/test/common/chatModel.test.ts | 1 - .../test/common/tools/manageTodoListTool.test.ts | 9 +++++---- .../contrib/debug/browser/breakpointsView.ts | 9 +++++---- 10 files changed, 38 insertions(+), 29 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 583cc820859..0a205b5febc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -274,18 +274,10 @@ export default tseslint.config( 'src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts', 'src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts', 'src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts', - 'src/vs/workbench/contrib/chat/common/annotations.ts', - 'src/vs/workbench/contrib/chat/common/chat.ts', - 'src/vs/workbench/contrib/chat/common/chatAgents.ts', 'src/vs/workbench/contrib/chat/common/chatModel.ts', - 'src/vs/workbench/contrib/chat/common/chatService.ts', - 'src/vs/workbench/contrib/chat/common/chatServiceImpl.ts', - 'src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts', - 'src/vs/workbench/contrib/chat/test/common/chatModel.test.ts', 'src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts', 'src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts', 'src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts', - 'src/vs/workbench/contrib/debug/browser/breakpointsView.ts', 'src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts', 'src/vs/workbench/contrib/debug/browser/variablesView.ts', 'src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts', diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index c7b98c6fa66..5e9a8cb4d23 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -7,6 +7,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; +import { isLocation } from '../../../../editor/common/languages.js'; import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from './chatModel.js'; import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from './chatService.js'; @@ -24,10 +25,10 @@ export function annotateSpecialMarkdownContent(response: Iterable boolean) | undefined): boolean | undefined { @@ -24,7 +24,7 @@ export function checkModeOption(mode: ChatModeKind, option: boolean | ((mode: Ch * we don't break existing chats */ export function migrateLegacyTerminalToolSpecificData(data: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData): IChatTerminalToolInvocationData { - if ('command' in data) { + if (isLegacyChatTerminalToolInvocationData(data)) { data = { kind: 'terminal', commandLine: { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 941bbd12c33..4789ef0b615 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -743,8 +743,12 @@ interface IOldSerializedChatAgentData extends Omit r instanceof ChatRequestAgentPart); - const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); - const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); + const agentPart = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); + const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); + const commandPart = parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); const requests = [...model.getRequests()]; const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, { agent: agentPart?.agent ?? defaultAgent, diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index 470c6554bc9..b059297fb05 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -14,6 +14,7 @@ import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modes import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; +import { isChatContentVariableReference } from './chatService.js'; import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js'; @@ -240,7 +241,7 @@ export class CodeBlockModelCollection extends Disposable { return; } - const uriOrLocation = 'variableName' in ref.reference ? + const uriOrLocation = isChatContentVariableReference(ref.reference) ? ref.reference.value : ref.reference; if (!uriOrLocation) { diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 88220e568ee..6bf9afae24e 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -263,7 +263,6 @@ suite('normalizeSerializableChatData', () => { assert.strictEqual(newData.creationDate, v1Data.creationDate); assert.strictEqual(newData.lastMessageDate, v1Data.creationDate); assert.strictEqual(newData.version, 3); - assert.ok('customTitle' in newData); }); test('v2', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts index 66d65a99690..54983142a6c 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts @@ -7,16 +7,17 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { createManageTodoListToolData } from '../../../common/tools/manageTodoListTool.js'; import { IToolData } from '../../../common/languageModelToolsService.js'; +import { IJSONSchema } from '../../../../../../base/common/jsonSchema.js'; suite('ManageTodoListTool Description Field Setting', () => { ensureNoDisposablesAreLeakedInTestSuite(); function getSchemaProperties(toolData: IToolData): { properties: any; required: string[] } { assert.ok(toolData.inputSchema); - // eslint-disable-next-line local/code-no-any-casts - const schema = toolData.inputSchema as any; - const properties = schema?.properties?.todoList?.items?.properties; - const required = schema?.properties?.todoList?.items?.required; + const schema = toolData.inputSchema; + const todolistItems = schema?.properties?.todoList?.items as IJSONSchema | undefined; + const properties = todolistItems?.properties; + const required = todolistItems?.required; assert.ok(properties, 'Schema properties should be defined'); assert.ok(required, 'Schema required fields should be defined'); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 03a1e72e133..6c86e01fcd5 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -60,6 +60,7 @@ import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; import * as icons from './debugIcons.js'; import { DisassemblyView } from './disassemblyView.js'; import { equals } from '../../../../base/common/arrays.js'; +import { hasKey } from '../../../../base/common/types.js'; const $ = dom.$; @@ -1823,7 +1824,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: } const appendMessage = (text: string): string => { - return ('message' in breakpoint && breakpoint.message) ? text.concat(', ' + breakpoint.message) : text; + return breakpoint.message ? text.concat(', ' + breakpoint.message) : text; }; if (debugActive && breakpoint instanceof Breakpoint && breakpoint.pending) { @@ -1835,7 +1836,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: if (debugActive && !breakpoint.verified) { return { icon: breakpointIcon.unverified, - message: ('message' in breakpoint && breakpoint.message) ? breakpoint.message : (breakpoint.logMessage ? localize('unverifiedLogpoint', "Unverified Logpoint") : localize('unverifiedBreakpoint', "Unverified Breakpoint")), + message: breakpoint.message ? breakpoint.message : (breakpoint.logMessage ? localize('unverifiedLogpoint', "Unverified Logpoint") : localize('unverifiedBreakpoint', "Unverified Breakpoint")), showAdapterUnverifiedMessage: true }; } @@ -1935,7 +1936,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: }; } - const message = ('message' in breakpoint && breakpoint.message) ? breakpoint.message : breakpoint instanceof Breakpoint && labelService ? labelService.getUriLabel(breakpoint.uri) : localize('breakpoint', "Breakpoint"); + const message = breakpoint.message ? breakpoint.message : breakpoint instanceof Breakpoint && labelService ? labelService.getUriLabel(breakpoint.uri) : localize('breakpoint', "Breakpoint"); return { icon: breakpointIcon.regular, message @@ -2047,7 +2048,7 @@ abstract class MemoryBreakpointAction extends Action2 { })); disposables.add(input.onDidAccept(() => { const r = this.parseAddress(input.value, true); - if ('error' in r) { + if (hasKey(r, { error: true })) { input.validationMessage = r.error; } else { resolve(r); From c561232a0a8f96c73b7bd9dc2039b00804284a44 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 23 Dec 2025 19:45:33 +1100 Subject: [PATCH 25/25] Support refreshing Chat Session Provider Options (#284815) --- .../api/browser/mainThreadChatSessions.ts | 31 +++++++++++--- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostChatSessions.ts | 6 +++ .../browser/mainThreadChatSessions.test.ts | 41 ++++++++++++++++++- .../vscode.proposed.chatSessionsProvider.d.ts | 8 ++++ 5 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index d7fdfe8ca8b..0b467feeba8 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -578,11 +578,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._sessionTypeToHandle.set(chatSessionScheme, handle); this._contentProvidersRegistrations.set(handle, this._chatSessionsService.registerChatSessionContentProvider(chatSessionScheme, provider)); - this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => { - if (options?.optionGroups && options.optionGroups.length) { - this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, options.optionGroups); - } - }).catch(err => this._logService.error('Error fetching chat session options', err)); + this._refreshProviderOptions(handle, chatSessionScheme); } $unregisterChatSessionContentProvider(handle: number): void { @@ -634,6 +630,31 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat // throw new Error('Method not implemented.'); } + $onDidChangeChatSessionProviderOptions(handle: number): void { + let sessionType: string | undefined; + for (const [type, h] of this._sessionTypeToHandle) { + if (h === handle) { + sessionType = type; + break; + } + } + + if (!sessionType) { + this._logService.warn(`No session type found for chat session content provider handle ${handle} when refreshing provider options`); + return; + } + + this._refreshProviderOptions(handle, sessionType); + } + + private _refreshProviderOptions(handle: number, chatSessionScheme: string): void { + this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => { + if (options?.optionGroups && options.optionGroups.length) { + this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, options.optionGroups); + } + }).catch(err => this._logService.error('Error fetching chat session options', err)); + } + override dispose(): void { for (const session of this._activeSessions.values()) { session.dispose(); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 11941ad8369..9e87b279614 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3305,6 +3305,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { $registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void; $unregisterChatSessionContentProvider(handle: number): void; $onDidChangeChatSessionOptions(handle: number, sessionResource: UriComponents, updates: ReadonlyArray): void; + $onDidChangeChatSessionProviderOptions(handle: number): void; $handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise; $handleAnchorResolve(handle: number, sessionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto): void; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 833bf30e20a..f9659471a27 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -151,6 +151,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio })); } + if (provider.onDidChangeChatSessionProviderOptions) { + disposables.add(provider.onDidChangeChatSessionProviderOptions(() => { + this._proxy.$onDidChangeChatSessionProviderOptions(handle); + })); + } + return new extHostTypes.Disposable(() => { this._chatSessionContentProviders.delete(handle); disposables.dispose(); diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 990cb4649c7..e6988bef563 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -19,7 +19,7 @@ import { ILogService, NullLogService } from '../../../../platform/log/common/log import { ChatSessionsService } from '../../../contrib/chat/browser/chatSessions.contribution.js'; import { IChatAgentRequest } from '../../../contrib/chat/common/chatAgents.js'; import { IChatProgress, IChatProgressMessage, IChatService } from '../../../contrib/chat/common/chatService.js'; -import { IChatSessionItem, IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js'; +import { IChatSessionItem, IChatSessionProviderOptionGroup, IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js'; import { LocalChatSessionUri } from '../../../contrib/chat/common/chatUri.js'; import { ChatAgentLocation } from '../../../contrib/chat/common/constants.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -517,4 +517,43 @@ suite('MainThreadChatSessions', function () { mainThread.$unregisterChatSessionContentProvider(1); }); + + test('$onDidChangeChatSessionProviderOptions refreshes option groups', async function () { + const sessionScheme = 'test-session-type'; + const handle = 1; + + const optionGroups1: IChatSessionProviderOptionGroup[] = [{ + id: 'models', + name: 'Models', + items: [{ id: 'modelA', name: 'Model A' }] + }]; + const optionGroups2: IChatSessionProviderOptionGroup[] = [{ + id: 'models', + name: 'Models', + items: [{ id: 'modelB', name: 'Model B' }] + }]; + + const provideOptionsStub = proxy.$provideChatSessionProviderOptions as sinon.SinonStub; + provideOptionsStub.onFirstCall().resolves({ optionGroups: optionGroups1 } as IChatSessionProviderOptions); + provideOptionsStub.onSecondCall().resolves({ optionGroups: optionGroups2 } as IChatSessionProviderOptions); + + mainThread.$registerChatSessionContentProvider(handle, sessionScheme); + + // Wait for initial options fetch triggered on registration + await new Promise(resolve => setTimeout(resolve, 0)); + + let storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme); + assert.ok(storedGroups); + assert.strictEqual(storedGroups![0].items[0].id, 'modelA'); + + // Simulate extension signaling that provider options have changed + mainThread.$onDidChangeChatSessionProviderOptions(handle); + await new Promise(resolve => setTimeout(resolve, 0)); + + storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme); + assert.ok(storedGroups); + assert.strictEqual(storedGroups![0].items[0].id, 'modelB'); + + mainThread.$unregisterChatSessionContentProvider(handle); + }); }); diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 772fc387b98..a90c9ecde7c 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -241,6 +241,14 @@ declare module 'vscode' { */ readonly onDidChangeChatSessionOptions?: Event; + /** + * Event that the provider can fire to signal that the available provider options have changed. + * + * When fired, the editor will re-query {@link ChatSessionContentProvider.provideChatSessionProviderOptions} + * and update the UI to reflect the new option groups. + */ + readonly onDidChangeChatSessionProviderOptions?: Event; + /** * Provides the chat session content for a given uri. *