feat(coding-agent): surface extension shortcut conflicts

This commit is contained in:
Mario Zechner 2026-01-24 02:55:31 +01:00
parent 72de8f26a1
commit 3a57f1259b
2 changed files with 38 additions and 3 deletions

View file

@ -6,6 +6,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { ImageContent, Model } from "@mariozechner/pi-ai";
import type { KeyId } from "@mariozechner/pi-tui";
import { type Theme, theme } from "../../modes/interactive/theme/theme.js";
import type { ResourceDiagnostic } from "../diagnostics.js";
import type { KeyAction, KeybindingsConfig } from "../keybindings.js";
import type { ModelRegistry } from "../model-registry.js";
import type { SessionManager } from "../session-manager.js";
@ -162,6 +163,7 @@ export class ExtensionRunner {
private forkHandler: ForkHandler = async () => ({ cancelled: false });
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
private shutdownHandler: ShutdownHandler = () => {};
private shortcutDiagnostics: ResourceDiagnostic[] = [];
constructor(
extensions: Extension[],
@ -275,30 +277,42 @@ export class ExtensionRunner {
}
getShortcuts(effectiveKeybindings: Required<KeybindingsConfig>): Map<KeyId, ExtensionShortcut> {
this.shortcutDiagnostics = [];
const builtinKeybindings = buildBuiltinKeybindings(effectiveKeybindings);
const extensionShortcuts = new Map<KeyId, ExtensionShortcut>();
const addDiagnostic = (message: string, extensionPath: string) => {
this.shortcutDiagnostics.push({ type: "warning", message, path: extensionPath });
if (!this.hasUI()) {
console.warn(message);
}
};
for (const ext of this.extensions) {
for (const [key, shortcut] of ext.shortcuts) {
const normalizedKey = key.toLowerCase() as KeyId;
const builtInKeybinding = builtinKeybindings[normalizedKey];
if (builtInKeybinding?.restrictOverride === true) {
console.warn(
addDiagnostic(
`Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`,
shortcut.extensionPath,
);
continue;
}
if (builtInKeybinding?.restrictOverride === false) {
console.warn(
addDiagnostic(
`Extension shortcut conflict: '${key}' is built-in shortcut for ${builtInKeybinding.action} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,
shortcut.extensionPath,
);
}
const existingExtensionShortcut = extensionShortcuts.get(normalizedKey);
if (existingExtensionShortcut) {
console.warn(
addDiagnostic(
`Extension shortcut conflict: '${key}' registered by both ${existingExtensionShortcut.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,
shortcut.extensionPath,
);
}
extensionShortcuts.set(normalizedKey, shortcut);
@ -307,6 +321,10 @@ export class ExtensionRunner {
return extensionShortcuts;
}
getShortcutDiagnostics(): ResourceDiagnostic[] {
return this.shortcutDiagnostics;
}
onError(listener: ExtensionErrorListener): () => void {
this.errorListeners.add(listener);
return () => this.errorListeners.delete(listener);

View file

@ -932,6 +932,23 @@ export class InteractiveMode {
this.chatContainer.addChild(new Spacer(1));
}
const extensionDiagnostics: ResourceDiagnostic[] = [];
const extensionErrors = this.session.resourceLoader.getExtensions().errors;
if (extensionErrors.length > 0) {
for (const error of extensionErrors) {
extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
}
}
const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
extensionDiagnostics.push(...shortcutDiagnostics);
if (extensionDiagnostics.length > 0) {
const warningLines = this.formatDiagnostics(extensionDiagnostics, metadata);
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
this.chatContainer.addChild(new Spacer(1));
}
// Show loaded themes (excluding built-in)
const loadedThemes = this.session.resourceLoader.getThemes().themes;
const customThemes = loadedThemes.filter((t) => t.sourcePath);