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 1/8] 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 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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; } }