Skip to content

feat(tui): suggest plugins from the prompt, not only /plugin suggest - #5663

Open
Hmbown wants to merge 2 commits into
mainfrom
grok/v0912-plugin-prompt-suggest-20260827
Open

feat(tui): suggest plugins from the prompt, not only /plugin suggest#5663
Hmbown wants to merge 2 commits into
mainfrom
grok/v0912-plugin-prompt-suggest-20260827

Conversation

@Hmbown

@Hmbown Hmbown commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Summary

The remaining #5579 UX: if someone writes a prompt about Supabase and they have that plugin (or a catalog that lists it), toast the next step instead of making them type /plugin suggest.

  • Sending a task ranks the prompt against installed plugins (name, keywords, skills, hosts) and any locally added marketplace catalogs.
  • Strong matches (keyword/name, not description-only) toast /plugin trust, /plugin enable, or /plugin marketplace install <catalog> <name>.
  • Already-active plugins stay quiet. Caps: one toast per plugin name, two per session. Never installs, trusts, or enables.
  • /plugin suggest uses the same ranking, including catalog candidates that are not installed yet.
  • On-disk bundle changes also nudge /plugin reload between turns (idle poll), not only on send.
  • Plugin skills already reach the model catalogue with descriptions once trusted and enabled; the test now asserts that.

Does not close #5579: ranking is still local metadata (no cheap-model matcher), and the disk check is a fingerprint poll, not inotify.

Testing

  • cargo fmt --all -- --check
  • focused codewhale-tui --lib tests for ranking, /plugin suggest, send-time toast, and plugin skill catalogue copy
  • python3 scripts/check-tui-locale-parity.py
  • cargo clippy --workspace --all-targets --all-features --locked (CI)
  • cargo test --workspace --all-features --locked (CI)
  • Verified TUI behavior manually if UI changes

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes
  • Harvested/co-authored credit uses a GitHub numeric noreply address

Sending a task now toasts the next review step when the prompt strongly
matches an installed-but-idle plugin or a marketplace catalog the user
added. Description-only matches stay quiet. /plugin suggest ranks
manifest keywords and catalog candidates the same way and still never
installs, trusts, or enables anything.

On-disk plugin changes also nudge /plugin reload between turns, not only
on send. Plugin skills already reach the model catalogue with their
descriptions once trusted and enabled; the test now asserts that.

Does not close #5579: ranking is still local metadata, not a model
route, and there is no inotify watcher.
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 3m 0s —— View job


Claude's review

  • Gather context (PR diff, repo instructions)
  • Understand the request (code review per custom instructions)
  • Review correctness bugs
  • Review provider/model/route safety implications
  • Review reuse/simplification opportunities
  • Review test coverage
  • Review security concerns
  • Post final review

Summary

Solid, well-scoped follow-up to #5579. The read-only/no-side-effect invariant (AlreadyActive/Inspect never toast, caps of 1/plugin + 2/session, marketplace suggestions never install) is consistently enforced across plugin_suggestions.rs and the /plugin suggest command. No provider/model/route-prefix inference in this diff — not applicable here. Locale strings for all 15 languages are genuinely translated, not copy-pasted English.

Findings

1. Proactive threshold can fire on generic single-word name fragments, not just real keyword/name matches (crates/tui/src/plugins/recommend.rs:19, scoring in crates/tui/src/skills/recommend.rs:78-81)

PROACTIVE_MIN_SCORE = 700 is documented as "keyword/name, not description-only" matches only. But recommend_one scores every word-boundary fragment of a hyphenated/multi-word plugin name at base 700 + term.len() (add_phrase_match(query, &term, "name", 700, ...) for each word_terms(name)), independent of any keyword match. For a plugin literally named e.g. code-review or git-tools, the word review or tools alone scores ≥700 — so a completely generic prompt like "review this PR" would proactively toast /plugin trust code-review, which is exactly the "nag on generic prompts" failure mode the PR/tests explicitly guard against for descriptions (generic_prompts_do_not_match_on_description_alone) but not for name-word fragments. Existing tests only exercise single-word plugin names (supabase), so this gap isn't covered. Consider requiring the top match to come from a keyword or the full name phrase (score ≥800, or check the match label) rather than any name-word fragment, for the proactive path specifically.

2. load_marketplace_candidates is duplicated verbatim (crates/tui/src/tui/plugin_suggestions.rs:73-89 and crates/tui/src/commands/groups/plugins/mod.rs:225-238)

Both functions open the MarketplaceStore at plugin_registry.state_path(), load it, and flat-map catalog.candidates identically. Worth hoisting into a single helper (e.g. on PluginRegistry or in plugins/recommend.rs, which both call sites already depend on) so the two suggestion surfaces (/plugin suggest and the send-time nudge) can't drift.

Nothing blocking; #1 is the one worth a second look before merge since it's a real precision gap in the "never nag" design goal.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92b28b49cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +105 to +107
for candidate in marketplace {
if candidate.has_errors() {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude unsupported candidates from install suggestions

When a catalog retains a valid candidate whose install_plan is MarketplaceInstallPlan::Unsupported—for example, a Codex entry marked NOT_AVAILABLEhas_errors() can still be false, so this candidate is ranked and presented as /plugin marketplace install .... The marketplace install handler then unconditionally rejects that command as unsupported, making both /plugin suggest and the proactive toast recommend an action that cannot succeed; filter on install_plan.is_supported() or present a non-install action instead.

Useful? React with 👍 / 👎.

Comment on lines +133 to +135
(
format!("installed:{}", plugin.name()),
RegistryEntry {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep index bookkeeping out of semantic name matching

When a prompt contains the ordinary word installed, such as “use the installed plugin to handle this,” recommend_remote_skills tokenizes this synthetic map key as a plugin name and awards every installed bundle a name-match score above the proactive threshold. The result can be a toast for an arbitrary alphabetically ranked inactive plugin despite no task-specific match; use an opaque key that the matcher does not score, or pass the actual plugin name separately from the collision-avoidance key.

Useful? React with 👍 / 👎.

Comment on lines +179 to +182
let index = RegistryDocument {
skills: entries.iter().cloned().collect::<BTreeMap<_, _>>(),
};
let ranked = recommend_remote_skills(task, &index, options.limit.saturating_mul(2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter active plugins before truncating proactive results

In proactive mode limit is 1, so this requests only the top two ranked entries before later discarding already-active plugins. If two active plugins rank ahead of another strongly matching inactive plugin, both retained entries are filtered out and no toast is produced even though an eligible match exists; exclude active entries before ranking or rank enough entries to apply the eligibility filters before enforcing the requested limit.

Useful? React with 👍 / 👎.

Comment on lines +233 to +235
let Ok(state) = store.load() else {
return Vec::new();
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface marketplace state failures from /plugin suggest

When marketplaces.json is malformed, has an unsupported schema, or fails the store's permission checks, store.load() returns an error but this helper silently treats it as an empty catalog. The explicit /plugin suggest command can consequently report that no catalog plugin matched even though it searched incomplete state, unlike the marketplace commands that surface the fail-closed error; propagate the error to the command while allowing the unsolicited-toast path to remain quiet if desired.

Useful? React with 👍 / 👎.

Comment on lines +63 to +66
if let Some(message) = crate::plugins::plugin_reload_nudge(
self.plugin_registry.as_ref(),
&mut self.plugin_reload_nudge_stamp,
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fingerprint plugin contents during idle reload polling

When an existing plugin.toml, skill, or other file inside a bundle is edited in place, PluginCatalogStamp::capture still records only the containing plugin directory's modification time; on normal filesystems, changing file contents does not update that directory timestamp. This newly added idle poll therefore never emits the advertised reload nudge for such rewrites and detects only directory-entry changes such as adding or removing a bundle; include relevant file metadata or content in the live fingerprint.

Useful? React with 👍 / 👎.

Comment on lines +194 to +196
.and_then(|candidate| candidate.description.clone())
.filter(|description| !description.trim().is_empty())
.unwrap_or_else(|| "Catalog plugin.".to_string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Localize the new catalog suggestion copy

When the UI locale is not English, the newly added catalog suggestion path still renders English fallback and state text such as Catalog plugin. and not installed (along with the new English-only result framing), producing a partially untranslated /plugin suggest response despite complete locale packs. Add typed MessageId entries for this new prose and compose only commands and glyphs in code.

AGENTS.md reference: crates/tui/AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

Keep the send-time toast. A Grok-style keyword matcher (word boundaries,
domains, longest keyword) drives a one-line composer CTA while you type:
Install {name} plugin? Review runs the existing /plugin trust,
marketplace-install, or /plugin install path and never auto-installs.
Dismiss stays dismissed for that name this session; already-active
plugins stay hidden.

Matching idle or catalog plugins also append a bounded
<recommended_plugins> user-turn block on send (not the pinned system
prefix). request_plugin_install surfaces that review for the human and
does not mutate disk.
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 24s —— View job


Claude's review

  • Gather context (new diff since last review, prior Claude + Codex feedback)
  • Check whether prior findings were addressed by the new commit
  • Review the new commit's own changes for correctness/safety/reuse/tests
  • Post final review

Summary

The new commit (d17b974) adds a live composer CTA driven by a fresh, stricter word-boundary matcher (crates/tui/src/plugins/matcher.rs) plus a read-only request_plugin_install tool — both well-scoped, fail-closed on unknown names, and covered by tests. It also fixed one prior finding: load_marketplace_candidates is no longer duplicated between plugin_suggestions.rs and the /plugin suggest command (now a single function in plugins/recommend.rs). The no-side-effect invariant (never installs/trusts/enables) holds across all the new surfaces I checked, including the mouse/escape wiring in mouse_ui.rs/composer_ui.rs/event_loop.rs.

However, three issues flagged in the prior review round on crates/tui/src/plugins/recommend.rs are still present in this commit, and one of them is now duplicated into more call sites:

Findings

1. Unsupported marketplace candidates are still recommended as installable (unaddressed, now in 2 more places)crates/tui/src/plugins/recommend.rs

MarketplaceInstallPlan::is_supported() (crates/tui/src/plugins/marketplace/types.rs:242) is never called from plugins/recommend.rs. Both index_entry_from_marketplace (used by /plugin suggest and the send-time toast) and the new idle_and_catalog_keyword_matches (used by the live CTA and the request_plugin_install tool) only filter on candidate.has_errors(), not on install_plan. A catalog candidate with MarketplaceInstallPlan::Unsupported (e.g. a zip/remote/gzip-tarball entry, per marketplace.rs:255-265) can still be ranked and surfaced with /plugin marketplace install <catalog> <name> as the suggested next step — but the actual install handler unconditionally rejects it (crates/tui/src/commands/groups/plugins/marketplace.rs:259-265, "cannot be installed by Codewhale"). This was flagged by Codex on the prior commit and is now reachable from 4 surfaces instead of 2 (suggest, toast, live CTA, tool). Worth filtering install_plan.is_supported() (or routing unsupported entries to an Inspect-style non-install step) in both index_entry_from_marketplace and idle_and_catalog_keyword_matches.

2. Proactive toast path still scores on generic name-word fragments, including its own bookkeeping key (unaddressed) — crates/tui/src/plugins/recommend.rs:330-355, scoring in crates/tui/src/skills/recommend.rs:78-81

The new precise matcher (matcher.rs) is used only for the live composer CTA. The send-time toast (maybe_nudge_plugin_for_promptrecommend_plugins_for_taskrecommend_from_entries) still goes through the older skills::recommend::recommend_remote_skills, which scores every word-boundary fragment of the registry key at ≥700 (PROACTIVE_MIN_SCORE). Two compounding problems here, both raised in the prior review round and still live:

  • index_entry_from_installed uses format!("installed:{}", plugin.name()) as the map key. word_terms("installed:supabase") yields ["installed", "supabase"], so the literal English word "installed" in a user's prompt (e.g. "use the installed plugin for this") scores every installed-but-idle plugin ≥709 — an accidental toast unrelated to the task.
  • Independent of that, any plugin whose real name is itself a common word or hyphenated compound (e.g. code-review, git-tools) will proactively toast on a generic prompt like "review this PR", since word_terms(name) scores each fragment at base 700 regardless of keyword match — the exact "nag on generic prompts" failure mode the existing generic_prompts_do_not_match_on_description_alone test guards against for descriptions, but not for name fragments.

Given matcher.rs already solves this correctly (whole-keyword/name matching with real word boundaries, no key leakage) for the live CTA, it'd be worth routing the proactive toast through it too instead of maintaining two divergent matchers with different precision guarantees for what's supposed to be the same "never nag" invariant.

3. /plugin suggest catalog/state copy is still English-only (unaddressed) — crates/tui/src/commands/groups/plugins/mod.rs

New literals introduced for the catalog-candidate path — "Catalog plugin.", "not installed" — join the pre-existing hardcoded /plugin suggest prose. All the new toast/CTA strings (PluginPromptSuggestTrust/Enable/Marketplace, PluginCtaInstallPrompt/Review/Dismiss) are properly localized across all 15 locales, so this command's copy is the one path in this feature area that's still an outlier.

Nits (not blocking)

Nothing here is a regression introduced by this commit — findings #1 and #2 carry over from the prior review round on 92b28b4 and are now reachable from more call sites; #3 is new copy following an existing English-only pattern in that command.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin UX parity with Claude Code: proactive recommendations, reload discoverability, hot-reload

1 participant