feat(plugin): surface loaded plugin commands in command-line autocomplete

Autocomplete suggestions came only from all_registered_command_names() —
the compile-time inventory registry returning &'static str — so
runtime-loaded plugin commands could never appear. Typing a plugin's
command prefix (e.g. `LS_`) showed nothing even with the plugin loaded
and its ribbon tab active; dispatch worked, only discovery was blind.

Collect each enabled plugin's ribbon ToolDef command ids plus its
manifest command_prefixes into a dynamic candidate pool
(OwnedRibbonGroup::command_ids → PluginManager::command_names →
plugin_command_names), refreshed in rebuild_ribbon_modules on startup
load, settings reload, and every enable/disable toggle. ranked_matches
now merges this pool with the static registry and returns owned strings.

A plugin.toml `commands = [...]` list for sub-verbs with no ribbon button
(e.g. LS_AUTOLABEL) stays a follow-up — it's a plugin-API surface change.

Closes #272

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-04 23:01:53 +03:00
1 parent 60872da62a
commit 77c0c5a9d0
8 files changed
+173 -20

No files matched your search

@@ -122,6 +122,24 @@ impl PluginManager {
.collect()
}
/// Command names advertised by every alive, non-disabled plugin: each
/// ribbon tool's command id plus the manifest's `command_prefixes`. The
/// host merges these into command-line autocomplete so plugin commands are
/// discoverable by typing (#272).
pub fn command_names<F: Fn(&str) -> bool>(&self, is_disabled: F) -> Vec<String> {
let mut out = Vec::new();
for p in &self.plugins {
if is_disabled(p.process.id()) || !p.process.is_alive() {
continue;
}
for group in p.process.ribbon() {
out.append(&mut group.command_ids());
}
out.extend(p.process.manifest().command_prefixes.iter().cloned());
}
out
}
/// Begin asynchronous shutdown of every plugin process.
///
/// Kills every child synchronously on the calling thread and moves the