feat(console): parameter builder in the command explorer (phase 3)

The detail pane's Parameters section is now an editable form: each parameter is
a type-hinted input field (string / number / json, optional marked), and a live
"Builds" preview shows the command being assembled. String values are
auto-quoted; a required field left empty keeps its placeholder and disables
"Insert & run"; trailing empty optionals are omitted.

Insert and Insert & run now use the assembled command (not just the template),
so Insert & run works for parameterized commands once the required fields are
filled — destructive commands still confirm first. Param fields reset when the
selected command changes or the modal opens. Keyboard nav (Up/Down/Enter) is
scoped to the search box so typing in a field doesn't move the selection.

A best-effort parseParamSpecs turns the human-readable param templates into
fields; JSON-array params (z_sendmany, createrawtransaction) surface as a single
"json" field the user pastes into. Adds the console_ref_builds label + 8
translations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 11:32:35 -05:00
parent 44a17f3ad0
commit 4416e01f9c
11 changed files with 144 additions and 44 deletions

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "Kein Daemon",
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
"console_quit_note": "'quit'/'exit' werden hier nicht benötigt — schließen Sie einfach das Fenster.",
"console_ref_builds": "Ergibt",
"console_ref_cancel": "Abbrechen",
"console_ref_destructive": "Folgenreich",
"console_ref_example": "Beispiel",

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "Sin daemon",
"console_not_connected": "Error: No conectado al daemon",
"console_quit_note": "'quit'/'exit' no son necesarios aquí — simplemente cierra la ventana.",
"console_ref_builds": "Genera",
"console_ref_cancel": "Cancelar",
"console_ref_destructive": "Delicado",
"console_ref_example": "Ejemplo",

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "Pas de daemon",
"console_not_connected": "Erreur : Non connecté au daemon",
"console_quit_note": "'quit'/'exit' ne sont pas nécessaires ici — fermez simplement la fenêtre.",
"console_ref_builds": "Génère",
"console_ref_cancel": "Annuler",
"console_ref_destructive": "Sensible",
"console_ref_example": "Exemple",

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "デーモンなし",
"console_not_connected": "エラー:デーモンに接続されていません",
"console_quit_note": "ここでは 'quit''exit' は不要です — ウィンドウを閉じるだけで構いません。",
"console_ref_builds": "生成",
"console_ref_cancel": "キャンセル",
"console_ref_destructive": "要注意",
"console_ref_example": "例",

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "데몬 없음",
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
"console_ref_builds": "생성",
"console_ref_cancel": "취소",
"console_ref_destructive": "주의",
"console_ref_example": "예시",

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "Sem daemon",
"console_not_connected": "Erro: Não conectado ao daemon",
"console_quit_note": "'quit'/'exit' não são necessários aqui — basta fechar a janela.",
"console_ref_builds": "Gera",
"console_ref_cancel": "Cancelar",
"console_ref_destructive": "Sensível",
"console_ref_example": "Exemplo",

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "Нет daemon",
"console_not_connected": "Ошибка: Не подключено к daemon",
"console_quit_note": "Здесь не нужны 'quit'/'exit' — просто закройте окно.",
"console_ref_builds": "Формирует",
"console_ref_cancel": "Отмена",
"console_ref_destructive": "Осторожно",
"console_ref_example": "Пример",

View File

@@ -229,6 +229,7 @@
"console_no_daemon": "无守护进程",
"console_not_connected": "错误:未连接到守护进程",
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
"console_ref_builds": "生成",
"console_ref_cancel": "取消",
"console_ref_destructive": "谨慎",
"console_ref_example": "示例",

View File

@@ -1495,6 +1495,49 @@ std::vector<std::string> splitParamTemplate(const char* params)
return out;
}
// A parsed parameter for the builder form. type in {string, number, json}; `raw` is the original
// template token (used as a placeholder when a required field is left empty).
struct ConsoleParamSpec {
std::string label;
std::string type;
bool optional = false;
std::string raw;
};
// Best-effort parse of a param template into fillable fields. The templates are human-readable, not
// a strict schema, so heuristics: [word] / ["word"] = an optional scalar; a [ / { with JSON content
// (a brace or comma) = a JSON field the user pastes; otherwise a required string/number.
std::vector<ConsoleParamSpec> parseParamSpecs(const char* params)
{
std::vector<ConsoleParamSpec> out;
for (const std::string& t : splitParamTemplate(params)) {
ConsoleParamSpec p;
p.raw = t;
std::string inner = t;
if (inner.size() >= 2 && inner.front() == '[' && inner.back() == ']') {
std::string body = inner.substr(1, inner.size() - 2);
bool looksJson = body.find('{') != std::string::npos ||
body.find(',') != std::string::npos ||
(!body.empty() && body.front() == '[');
if (!looksJson) { p.optional = true; inner = body; } // optional scalar wrapper
}
char c0 = inner.empty() ? 0 : inner.front();
if (c0 == '"' || c0 == '\'') {
p.type = "string";
if (inner.size() >= 2 && inner.back() == c0) inner = inner.substr(1, inner.size() - 2);
p.label = inner;
} else if (c0 == '{' || c0 == '[') {
p.type = "json";
p.label = "json";
} else {
p.type = "number";
p.label = inner;
}
out.push_back(p);
}
return out;
}
// Translate a command-category name from the static reference tables (English) for display. The
// per-command descriptions stay in English (technical RPC docs); only the 7 category labels are i18n'd.
const char* consoleCategoryLabel(const char* name)
@@ -1554,35 +1597,72 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
ImGui::PopTextWrapPos();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Parameters — each token with a rough type tag; optional ([...]) tokens are dimmed.
// Parameters — an editable form that assembles the command. Reset the fields whenever the
// selected command changes so values don't carry across commands.
if (cmd_param_owner_ != &cmd) {
for (auto& b : cmd_param_bufs_) b[0] = '\0';
cmd_param_owner_ = &cmd;
}
std::vector<ConsoleParamSpec> specs = parseParamSpecs(cmd.params);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_parameters"));
std::vector<std::string> toks = splitParamTemplate(cmd.params);
if (toks.empty()) {
if (specs.empty()) {
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), "%s", TR("console_ref_no_params"));
} else {
ImGui::Indent(Layout::spacingMd());
for (const std::string& t : toks) {
bool optional = (!t.empty() && t.front() == '[');
char c0 = t.empty() ? 0 : t[0];
if (optional && t.size() >= 2) c0 = t[1];
const char* type = (c0 == '"' || c0 == '\'') ? "string"
: (c0 == '{' || c0 == '[') ? "json" : "number";
float labelW = 110.0f * dp;
ImGui::Indent(Layout::spacingSm());
for (size_t k = 0; k < specs.size() && k < 6; k++) {
const ConsoleParamSpec& s = specs[k];
ImGui::PushID((int)k);
ImGui::PushFont(mono);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(optional ? OnSurfaceMedium() : OnSurface()),
"%s", t.c_str());
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(s.optional ? OnSurfaceMedium() : OnSurface()),
"%s", s.label.c_str());
ImGui::PopFont();
ImGui::SameLine();
if (optional)
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
" %s \xC2\xB7 %s", type, TR("console_ref_optional"));
else
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()), " %s", type);
ImGui::SameLine(labelW);
ImGui::SetNextItemWidth(-1);
std::string hint = s.optional
? (s.type + " \xC2\xB7 " + std::string(TR("console_ref_optional"))) : s.type;
ImGui::InputTextWithHint("##pv", hint.c_str(), cmd_param_bufs_[k], sizeof(cmd_param_bufs_[k]));
ImGui::PopID();
}
ImGui::Unindent(Layout::spacingMd());
ImGui::Unindent(Layout::spacingSm());
}
// Assemble the command from the filled fields. A required field left empty keeps its placeholder
// token (so Insert still shows what's needed) and marks the command incomplete (Insert & run
// disabled). String values are auto-quoted; trailing empty optionals are omitted.
auto trimStr = [](const std::string& s) -> std::string {
size_t a = s.find_first_not_of(" \t");
if (a == std::string::npos) return std::string();
return s.substr(a, s.find_last_not_of(" \t") - a + 1);
};
bool complete = specs.size() <= 6;
std::string built = cmd.name;
for (size_t k = 0, stop = 0; k < specs.size() && k < 6 && !stop; k++) {
std::string val = trimStr(cmd_param_bufs_[k]);
if (val.empty()) {
if (specs[k].optional) stop = 1; // omit this + trailing optionals
else { built += " " + specs[k].raw; complete = false; } // required placeholder
} else {
if (specs[k].type == "string" && val.front() != '"' && val.front() != '\'' &&
val.front() != '[' && val.front() != '{')
val = "\"" + val + "\"";
built += " " + val;
}
}
// Live "Builds" preview when the command takes parameters.
if (!specs.empty()) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_builds"));
ImGui::PushFont(mono);
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(IM_COL32(150, 200, 150, 255)), "%s", built.c_str());
ImGui::PopTextWrapPos();
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Example (curated commands only).
// Example (curated commands only) — reference alongside the builder.
if (cmd.example[0]) {
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("console_ref_example"));
ImGui::PushFont(mono);
@@ -1593,7 +1673,7 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
}
// Actions (or the destructive run confirmation).
// Actions (or the destructive run confirmation) — Insert/run use the assembled command.
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (run_confirm_cmd_ == &cmd) {
ImVec4 warn = ImGui::ColorConvertU32ToFloat4(Warning());
@@ -1607,28 +1687,32 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
if (material::TactileButton(TR("console_ref_cancel"), ImVec2(cbw, 0))) run_confirm_cmd_ = nullptr;
ImGui::SameLine();
if (material::TactileButton(TR("console_ref_run"), ImVec2(cbw, 0))) {
pending_submit_ = cmd.name;
pending_submit_ = built;
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
} else {
if (material::TactileButton(TR("console_ref_insert"), ImVec2(200.0f * dp, 0)))
insertCommandToInput(cmd);
// "Insert & run" only for commands that take no parameters — running a template with
// placeholders ("address", amount) would just error.
if (cmd.params[0] == '\0') {
ImGui::SameLine();
if (material::TactileButton(TR("console_ref_insert_run"), ImVec2(140.0f * dp, 0))) {
if (cmd.destructive) {
run_confirm_cmd_ = &cmd;
} else {
pending_submit_ = cmd.name;
command_search_[0] = '\0';
show_commands_popup_ = false;
}
if (material::TactileButton(TR("console_ref_insert"), ImVec2(200.0f * dp, 0))) {
snprintf(input_buffer_, sizeof(input_buffer_), "%s", built.c_str());
command_search_[0] = '\0';
run_confirm_cmd_ = nullptr;
show_commands_popup_ = false;
}
// Insert & run: enabled once every required field is filled (a template with unfilled
// placeholders would just error).
ImGui::SameLine();
ImGui::BeginDisabled(!complete);
if (material::TactileButton(TR("console_ref_insert_run"), ImVec2(140.0f * dp, 0))) {
if (cmd.destructive) {
run_confirm_cmd_ = &cmd;
} else {
pending_submit_ = built;
command_search_[0] = '\0';
show_commands_popup_ = false;
}
}
ImGui::EndDisabled();
}
}
@@ -1652,11 +1736,15 @@ void ConsoleTab::renderCommandsPopup()
else show_commands_popup_ = false;
}
// Search box — auto-focus on open so the user can type immediately.
// Search box — auto-focus on open so the user can type immediately. Enter inserts the selected
// command's template; reset the param-builder fields on open.
if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(); cmd_param_owner_ = nullptr; }
ImGui::SetNextItemWidth(-1);
if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
bool searchChanged = ImGui::InputTextWithHint("##CmdSearch", TR("console_ref_search_hint"),
command_search_, sizeof(command_search_));
bool searchEnter = ImGui::InputTextWithHint("##CmdSearch", TR("console_ref_search_hint"),
command_search_, sizeof(command_search_),
ImGuiInputTextFlags_EnterReturnsTrue);
bool searchChanged = ImGui::IsItemEdited();
bool searchActive = ImGui::IsItemActive();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
std::string q(command_search_);
@@ -1693,15 +1781,16 @@ void ConsoleTab::renderCommandsPopup()
else { cmd_sel_cat_ = cmd_sel_idx_ = -1; }
}
// Keyboard nav: Up/Down move the selection, Enter inserts (disabled while a run-confirm shows).
if (!run_confirm_cmd_ && !order.empty() && selPos >= 0) {
// Keyboard nav from the SEARCH box only (so typing in a param field doesn't move the selection):
// Up/Down move, Enter inserts the selected command's template. Disabled while a run-confirm shows.
if (!run_confirm_cmd_ && searchActive && !order.empty() && selPos >= 0) {
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && selPos + 1 < (int)order.size()) selPos++;
else if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && selPos > 0) selPos--;
cmd_sel_cat_ = order[selPos].first;
cmd_sel_idx_ = order[selPos].second;
if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter))
insertCommandToInput(categories[cmd_sel_cat_].commands[cmd_sel_idx_]);
}
if (searchEnter && !run_confirm_cmd_ && cmd_sel_cat_ >= 0)
insertCommandToInput(categories[cmd_sel_cat_].commands[cmd_sel_idx_]);
// Two-pane body sized above the footer.
float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingXs();

View File

@@ -193,6 +193,8 @@ private:
int cmd_sel_idx_ = -1; // detail-pane selection: command index within that category
std::string pending_submit_; // command to run next frame (deferred so the modal needs no executor)
const ConsoleCommandEntry* run_confirm_cmd_ = nullptr; // destructive "Insert & run" awaiting confirmation
char cmd_param_bufs_[6][256] = {{0}}; // parameter-builder input fields
const ConsoleCommandEntry* cmd_param_owner_ = nullptr; // command the param buffers currently hold
};
} // namespace ui

View File

@@ -1306,6 +1306,7 @@ void I18n::loadBuiltinEnglish()
strings_["console_ref_no_params"] = "Takes no parameters.";
strings_["console_ref_optional"] = "optional";
strings_["console_ref_example"] = "Example";
strings_["console_ref_builds"] = "Builds";
strings_["console_ref_destructive"] = "Consequential";
strings_["console_ref_run_confirm"] = "Run %s now? This is a consequential command.";
strings_["console_ref_cancel"] = "Cancel";