fix(extensions): show warning when replaceable builtin replaced (#10174)

This commit is contained in:
Cristina Poncela Cubeiro 2026-09-29 16:37:51 +02:00 • committed by GitHub
parent 9a100c7cc4
commit 9d1a650352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -117,14 +117,37 @@ export function isBuiltinExtension(input: InlineExtension): input is BuiltinExte
* with another extension. For example, a third-party MCP extension that registers `/mcp` replaces
* the built-in MCP extension instead of both connecting the same servers.
*/
function omitReplacedExtensions(extensions: Extension[]): Extension[] {
function omitReplacedExtensions(
extensions: Extension[],
warnings?: Array<{ path: string; warning: string }>,
): Extension[] {
const names = (extension: Extension) => [
...[...extension.tools.keys()].map((name) => `tool:${name}`),
...[...extension.commands.keys()].map((name) => `command:${name}`),
...[...extension.flags.keys()].map((name) => `flag:${name}`),
];
const taken = new Set(extensions.filter((extension) => !extension.replaceable).flatMap(names));
return extensions.filter((extension) => !extension.replaceable || !names(extension).some((name) => taken.has(name)));
const taken = new Map(
extensions
.filter((extension) => !extension.replaceable)
.flatMap((extension) => names(extension).map((name) => [name, extension] as const)),
);
return extensions.filter((extension) => {
if (!extension.replaceable) return true;
const replacement = names(extension)
.map((name) => ({ name, extension: taken.get(name) }))
.find((value): value is { name: string; extension: Extension } => value.extension !== undefined);
if (!replacement) return true;
if (extension.path.startsWith(BUILTIN_PATH_PREFIX)) {
const builtinName = extension.path.slice(BUILTIN_PATH_PREFIX.length);
const [kind, rawName] = replacement.name.split(":", 2);
const registeredName = kind === "command" ? `/${rawName}` : kind === "flag" ? `--${rawName}` : rawName;
warnings?.push({
path: extension.path,
warning: `Extension ${replacement.extension.path} registers ${kind} \`${registeredName}\`, so built-in extension \`${builtinName}\` was not loaded. To use \`${builtinName}\`, run \`pi config\` and make sure it is enabled under Built-in extensions, then disable or remove the existing extension. We recommend only having one or the other loaded at a time.`,
});
}
return false;
});
}
export interface ResourceLoader {
@ -670,7 +693,9 @@ export class DefaultResourceLoader implements ResourceLoader {
const inlineExtensions = await this.loadExtensionFactories(extensionsResult.runtime);
extensionsResult.extensions.push(...inlineExtensions.extensions);
extensionsResult.errors.push(...inlineExtensions.errors);
extensionsResult.extensions = omitReplacedExtensions(extensionsResult.extensions);
const replacementWarnings: Array<{ path: string; warning: string }> = [];
extensionsResult.extensions = omitReplacedExtensions(extensionsResult.extensions, replacementWarnings);
mergeExtensionWarnings(extensionsResult, replacementWarnings);
return extensionsResult;
}
@ -743,12 +768,14 @@ export class DefaultResourceLoader implements ResourceLoader {
.filter((extension): extension is Extension => extension !== undefined);
orderedExtensions.push(...inlineExtensions.extensions);
const replacementWarnings: Array<{ path: string; warning: string }> = [];
const extensionsResult: LoadExtensionsResult = {
extensions: omitReplacedExtensions(orderedExtensions),
extensions: omitReplacedExtensions(orderedExtensions, replacementWarnings),
errors: [...(preTrustExtensions?.errors ?? []), ...remainingExtensions.errors, ...inlineExtensions.errors],
warnings: [...(preTrustExtensions?.warnings ?? []), ...(remainingExtensions.warnings ?? [])],
runtime: remainingExtensions.runtime,
};
mergeExtensionWarnings(extensionsResult, replacementWarnings);
this.addExtensionConflictDiagnostics(extensionsResult);
return extensionsResult;
}