Skip to content

console: intelligent command auto-resolution from user-typed prefix #162

Description

@havokentity

Context

Console UX nit: when the user types a prefix that doesn't exact-match a registered command/cvar, the console currently rejects it. The user has to type a longer prefix, hit Tab to autocomplete, then add the args. Friction adds up when 200+ cvars exist.

Goal

Intelligent partial-match resolution at command parse time: if the typed token isn't exact-match, find the BEST prefix match (longest common prefix that's unambiguous) and run that command transparently, with a clear log line showing what we picked.

Examples

> deno metalfx
[console] resolved `deno` -> `r_denoiser` (best prefix match of 1)
r_denoiser=metalfx

> sky proc
[console] resolved `sky` -> `r_sky_mode` (top match; 7 candidates start with `sky`)
r_sky_mode=procedural

> phys_drop_sph 0 1 0
[console] resolved `phys_drop_sph` -> `phys_drop_sphere`
(spawned sphere at 0,1,0)

> r_bloom 1
(no resolution needed; exact match)

When NOT to auto-resolve

If multiple candidates are equally close (no unambiguous winner), fall back to current behavior (error + suggest):

> p
[console] ambiguous prefix `p`: phys_clear, phys_drop_sphere, phys_drop_box, phys_status,
          prim_sphere, prim_plane, prim_light, ... (12 more)
          type more characters to disambiguate

Heuristics

Priority for tie-breaking:

  1. Exact match wins immediately, no resolution log line printed.
  2. Longest unique prefix (substring at position 0).
  3. If tied, prefer cvars over commands (cvars are the higher-frequency interaction).
  4. If still tied, prefer the alphabetically-first (deterministic).

Implementation sketch

Single function in src/console/Console.cpp:

struct Resolution {
    std::string canonical_name;   // empty if no resolution
    std::vector<std::string> ambiguous_matches;  // populated when no unique winner
    bool is_exact_match = false;
};

Resolution ResolveCommand(std::string_view typed);

Called from Console::Execute() before the dispatch. If canonical_name is non-empty AND !is_exact_match, print one info-level log line:

[console] resolved `<typed>` -> `<canonical_name>` (top match)

Then dispatch the canonical name with the remaining arg tokens unchanged.

If ambiguous_matches.size() > 1, print the error path (current behavior + list of candidates capped at ~8 with ... (N more) suffix).

Cvar (optional UX)

r_console_smart_resolve (CVAR_ARCHIVE, default 1): toggle the feature for users who prefer strict matching. Off = current behavior. On = the new auto-resolve.

Acceptance criteria

  • deno metalfx resolves to r_denoiser metalfx with one log line.
  • p (ambiguous) errors with the candidate list.
  • r_denoiser (exact match) dispatches with no resolution log line.
  • All existing exact-match commands keep working identically.
  • New doctest in tests/cvar_ux_test.cpp extending PR feat(console): platform-filter cvar listing + accept "0" as off alias #159's coverage with 3-4 cases (exact match, unique-prefix match, ambiguous, no-match).

Out of scope

  • Levenshtein / fuzzy-match across typos (r_denosier -> r_denoiser). Prefix-only for now; fuzzy is a separate issue.
  • Multi-word command resolution (we don't have multi-word commands today).

Cross-link

Fits naturally in the same PR as issue #161 (cvar UX: per-value platform gating + dep warnings) since both touch the cvar/command parse path. Bundling them lands the full console UX upgrade as one review unit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions