feat(lite): make the Console tab interactive (run backend commands)

The lite backend's litelib_execute() is the same command interface as
silentdragonxlite-cli (balance, info, height, list, notes, addresses, sync,
syncstatus, new, send, shield, encrypt, …), so the lite Console can be a real
interactive console — like the full-node RPC console — instead of a read-only
diagnostics log.

Controller: add an async arbitrary-command runner mirroring the broadcast
pattern — runConsoleCommand() splits "<command> [args]" (the first token is the
command, the remainder is passed as the single arg string litelib_execute
expects, since it does NOT whitespace-split), runs the bridge call on a detached
thread that captures the shared bridge (never `this`), and delivers the result
to a main-thread slot drained by takeConsoleResult(). Results are NEVER routed
through LiteDiagnostics (seed/export can return secrets).

Console tab: a command input (Enter to run, Up/Down history via the shared
console_input_model helpers) over a unified scroll buffer that interleaves the
automatic diagnostics events with user command I/O, colour-coded, with the live
status header preserved. The input is disabled while a command runs.

Two backend footguns are intercepted at the UI layer before forwarding:
`clear` (the backend command WIPES wallet tx history — re-bound to clearing the
view, what the user expects) and `quit`/`exit` (would only save; the embedded
backend must stay running with the app).

Test: runConsoleCommand drives the fake backend (info -> raw response; "new zs"
-> exercises the command/arg split; blank line rejected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:32:03 -05:00
parent c8183241c3
commit 4a65dce947
5 changed files with 294 additions and 26 deletions

View File

@@ -64,6 +64,15 @@ struct LiteNewAddressResult {
std::string error;
};
// Result of an interactive console command (lite Console tab). `response` is the raw backend
// output (JSON or text). It may contain SECRET material (e.g. seed/export) because the user can
// run any command, so it is shown only in the console view and NEVER routed through LiteDiagnostics.
struct LiteConsoleResult {
std::string command; // the command line that was run, echoed back
std::string response; // raw backend response (or an error message)
bool ok = false;
};
// A single send recipient. amountZatoshis is in zatoshis (1e-8 DRGX); memo is ignored by the
// backend for transparent destination addresses.
struct LiteSendRecipient {
@@ -227,6 +236,21 @@ public:
bool broadcastInProgress() const { return broadcastRunning_ && broadcastRunning_->load(); }
bool takeBroadcastResult(LiteBroadcastResult& out);
// --- Interactive console (lite Console tab) ---------------------------------------------
// Run an arbitrary backend command — the same verbs as silentdragonxlite-cli (balance, info,
// height, list, notes, addresses, sync, syncstatus, new, send, shield, encrypt, …). ASYNC:
// some commands (sync/rescan/send/shield/import) block, so the bridge call runs on a detached
// thread (captures the shared bridge, never `this`) and the result is delivered to a main-
// thread slot drained by takeConsoleResult(). `commandLine` is "<command> [args]" — the first
// whitespace-delimited token is the command and the remainder is passed as the single arg
// string the backend expects (litelib_execute does not split args; use the JSON form for send).
// Returns false if a console command is already running or no backend is linked. The caller
// (tab) intercepts `clear`/`quit` BEFORE calling this — backend `clear` wipes wallet history.
// Responses may contain secrets and are NEVER logged to LiteDiagnostics.
bool runConsoleCommand(std::string commandLine);
bool consoleCommandInProgress() const { return consoleRunning_ && consoleRunning_->load(); }
bool takeConsoleResult(LiteConsoleResult& out);
// Synchronous cores for send/shield (block on the backend; safe off the UI thread). Used by
// the async entry points above and directly by tests.
LiteBroadcastResult sendTransactionBlocking(const LiteSendRequest& request);
@@ -317,6 +341,14 @@ private:
std::shared_ptr<std::optional<LiteBroadcastResult>> broadcastResult_ =
std::make_shared<std::optional<LiteBroadcastResult>>();
// Detached interactive-console command runner (same shared-lifetime pattern as broadcast:
// captures the shared bridge + running flag + result slot, never `this`).
std::thread consoleThread_;
std::shared_ptr<std::atomic<bool>> consoleRunning_ = std::make_shared<std::atomic<bool>>(false);
std::shared_ptr<std::mutex> consoleResultMutex_ = std::make_shared<std::mutex>();
std::shared_ptr<std::optional<LiteConsoleResult>> consoleResult_ =
std::make_shared<std::optional<LiteConsoleResult>>();
// Asynchronous open with server failover (mirrors the sync/broadcast shared-lifetime pattern:
// the detached thread captures only shared_ptrs + value copies, never `this`, so it can
// safely outlive the controller). pumpAsyncOpen() finalizes the result on the main thread.