fix(console): don't silently drop a filled param after an empty optional

The builder's "omit trailing optionals" loop stopped at the FIRST empty
optional, which discarded any later field the user had filled — e.g. setban
with bantime empty but absolute=true built `setban "ip" "add"`, dropping the
typed `true`, and (worse) left Insert & run enabled so it ran a command that
ignored the input.

Include fields up to the last one that is filled or required; only genuinely
trailing empty optionals are dropped. A gap (an empty field before a later
filled one) keeps its placeholder and marks the command incomplete, so Insert
& run disables and nothing typed is lost. Found by an adversarial pass over all
82 param templates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 11:38:25 -05:00
parent 4416e01f9c
commit b34069f57c

View File

@@ -1635,13 +1635,20 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
if (a == std::string::npos) return std::string(); if (a == std::string::npos) return std::string();
return s.substr(a, s.find_last_not_of(" \t") - a + 1); return s.substr(a, s.find_last_not_of(" \t") - a + 1);
}; };
// Include fields up to the last one that is filled OR required; trailing empty optionals are
// dropped. An empty field that must still be included (a blank required field, or a gap before a
// later filled field — positional args can't skip a middle slot) keeps its placeholder and marks
// the command incomplete, so Insert & run stays disabled and no typed value is silently lost.
int lastNeeded = -1;
for (size_t k = 0; k < specs.size() && k < 6; k++)
if (!trimStr(cmd_param_bufs_[k]).empty() || !specs[k].optional) lastNeeded = (int)k;
bool complete = specs.size() <= 6; bool complete = specs.size() <= 6;
std::string built = cmd.name; std::string built = cmd.name;
for (size_t k = 0, stop = 0; k < specs.size() && k < 6 && !stop; k++) { for (int k = 0; k <= lastNeeded; k++) {
std::string val = trimStr(cmd_param_bufs_[k]); std::string val = trimStr(cmd_param_bufs_[k]);
if (val.empty()) { if (val.empty()) {
if (specs[k].optional) stop = 1; // omit this + trailing optionals built += " " + specs[k].raw;
else { built += " " + specs[k].raw; complete = false; } // required placeholder complete = false;
} else { } else {
if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' && if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
val.front() != '[' && val.front() != '{') val.front() != '[' && val.front() != '{')