Skip to content

Did-you-mean command suggestions (#3027)#3029

Merged
tastybento merged 4 commits into
developfrom
feature/3027-did-you-mean
Jul 11, 2026
Merged

Did-you-mean command suggestions (#3027)#3029
tastybento merged 4 commits into
developfrom
feature/3027-did-you-mean

Conversation

@tastybento

Copy link
Copy Markdown
Member

Implements the first slice of #3027: the did-you-mean layer, at both interception points.

What players see

Player types They get
/teams Did you mean /oneblock team? Click here or type yes to run it. (clickable)
/team invite Floris Did you mean /oneblock team invite Floris? — arguments survive
/oneblock invit Floris Did you mean /oneblock team invite Floris? — instead of the unknown-command error
/teams with several game modes installed, standing at spawn a short clickable list of the plausible options
/teams with several game modes installed, standing in the OneBlock world one confident suggestion for /oneblock team — world context is decisive
gibberish vanilla behavior, no false positives

Accepting is a click ([run_command:] inline tag through the normal locale pipeline) or typing yes/y in chat within 30 seconds. Pending suggestions are dropped when the player runs any other command or quits.

How it works

  • CommandMatcher (new, pure function, unit-tested standalone): matches typed tokens against the labels and aliases of every node in the command trees — because players type subcommands as if they were whole commands. Heuristics: exact > prefix > edit distance, scaled by length; ranking bonuses for the game mode world the player is standing in; permission-filtered; hidden/console-only/help commands never suggested.
  • SuggestionsManager (new): presentation and pending-intent tracking. One clear winner → single suggestion + typed-yes intent; several plausible → up to 3 clickable options.
  • DidYouMeanListener (new): listens to UnknownCommandEvent — which fires only when no plugin owns the command, so BentoBox never shadows another plugin — and replaces the vanilla "Unknown command" message. Chat listener consumes yes; command-preprocess and quit listeners clear pending intents.
  • CompositeCommand.execute: when the dispatch walk stops with unconsumed args at a node with subcommands (previously a guaranteed help dump/error), the suggestion engine gets first refusal. Fully backward-compatible: if nothing plausible matches, the old path runs unchanged.

Config

Two new toggles, default on (config.ymlgeneral.did-you-mean.unknown-commands / .subcommands).

Locales

The three new general.did-you-mean keys are translated into all 22 locale files. While syncing, the pre-existing gap commands.admin.team.setowner.specify-island (missing from every non-English file) was filled too. All files pass placeholder_check.py against en-US.

Demo

DidYouMeanScenarioTest follows the support transcript that motivated #3027 line by line (/teams/team invite/spawn):

./gradlew test --tests "world.bentobox.bentobox.suggestions.DidYouMeanScenarioTest" \
               --tests "world.bentobox.bentobox.suggestions.CommandMatcherTest"

21 tests, including: the transcript cases, typed-yes acceptance, click-event assertions on the actual Components, world-context disambiguation, config toggles, permission filtering, and no-false-positive checks.

Closes #3027 (first slice: both interception points + matcher; telemetry hooks come with #3022).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jbkc3n7vnNbtMx3s9dbkbc

tastybento and others added 4 commits July 11, 2026 11:49
New players routinely type a subcommand as if it were the whole command
(/teams or /team invite instead of /oneblock team) or mistype a
subcommand (/island invit). Instead of "Unknown command" or a help dump,
BentoBox now matches the input against every node of its command trees
and replies with a clickable suggestion the player can also accept by
typing "yes".

- CommandMatcher: pure matching engine - label/alias matching at any
  tree depth, edit-distance and prefix heuristics, world-context
  ranking, permission filtering
- SuggestionsManager: presentation and pending-intent tracking; single
  confident suggestion or a short clickable options list
- DidYouMeanListener: UnknownCommandEvent interception (fires only when
  no plugin owns the command), chat "yes" acceptance, pending cleanup
- CompositeCommand dispatch: unmatched subcommand args route through the
  suggestion engine before falling back to the old error
- Config: general.did-you-mean.unknown-commands / .subcommands (default
  on)
- Locales: new general.did-you-mean keys translated into all 22
  languages; also fills the pre-existing gap
  commands.admin.team.setowner.specify-island everywhere

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jbkc3n7vnNbtMx3s9dbkbc
In-game testing showed the clickable suggestions rendering their closing
tags as literal text, with the click event dead. Root cause: inline
[run_command]/[hover] commands were re-encoded as MiniMessage tags
wrapped around the message, but the message text contains a §r from the
legacy round-trip (any closed color emits one). §r maps to <reset>,
which closes the wrapping <click>/<hover> tags too - killing the events
and orphaning the closers, which MiniMessage renders as literal text.

Fix: extract the inline commands as data (Util.extractInlineCommands)
and apply ClickEvent/HoverEvent programmatically to the parsed
Component in User.parseToComponent. The events now cover the whole
message regardless of internal resets.
convertInlineCommandsToMiniMessage is kept (refactored over the new
extraction) for API compatibility.

Regression test asserts no literal tag text leaks and the click covers
the message on both the single-suggestion and options-list paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jbkc3n7vnNbtMx3s9dbkbc
- User.parseToComponent (S2637): assign nullable record accessor to a
  local so the null-check narrows it for @nonnull parseMiniMessageOrLegacy
- Util.extractInlineCommands (S6916 x2): replace the if statements inside
  the string-constant switch arms with first-wins ternary assignments
- CommandMatcher.quality (S3358): extract the nested ternary into an
  allowedDistance helper
- CommandMatcherTest (S5976): merge three identical matchCommandLine tests
  into one parameterized test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7UmQDCP5MGHEZqFiw44zH
@sonarqubecloud

Copy link
Copy Markdown

@tastybento tastybento merged commit 2605563 into develop Jul 11, 2026
3 checks passed
@tastybento tastybento deleted the feature/3027-did-you-mean branch July 11, 2026 22:47
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.

Design: did-you-mean layer — intercept unknown commands and suggest the intended BentoBox command

1 participant