localharness 0.30.0

A Rust-native agent SDK with pluggable LLM backends (Gemini today). Streaming, custom tools, safety policies, background triggers — zero external binaries.
Documentation
//! Layout + reset + pricing — panel toggles, the typed-confirmation reset,
//! pricing save, and key clear.

use crate::app::{dom, templates};
use crate::filesystem::Filesystem;

/// Pure DOM class flip on `#layout` — used by the panel toggles
/// (files-collapsed, financial-collapsed) so a collapse + expand
/// doesn't lose any panel state (open file viewer, pricing edit
/// in-flight, etc.). CSS handles the actual hide/show.
pub(super) fn toggle_layout_class(class: &str) {
    let Some(layout) = dom::by_id("layout") else { return };
    let current = layout.class_name();
    let trimmed = current.trim();
    let parts: Vec<&str> = trimmed.split_whitespace().collect();
    let new_cls = if parts.contains(&class) {
        parts.iter().filter(|c| **c != class).copied().collect::<Vec<_>>().join(" ")
    } else if parts.is_empty() {
        class.to_string()
    } else {
        format!("{} {class}", parts.join(" "))
    };
    layout.set_class_name(&new_cls);
}

/// Inline-confirmed reset: clear local app data at OPFS root, reload.
/// Identity-preserving — keeps the seed + owner hint (see the loop below).
/// Replaces the old `window.confirm()` flow per [[feedback-no-js-alerts]].
pub(super) fn reset_confirm_pressed() {
    // Typed confirmation — reset still clears app data/keys, so require the
    // literal word, not just a second click. (It no longer touches the seed.)
    let typed = dom::input_by_id("reset-confirm-text")
        .map(|i| i.value().trim().to_string())
        .unwrap_or_default();
    if !typed.eq_ignore_ascii_case("RESET") {
        dom::swap_inner(
            "reset-confirm-msg",
            "<span style=\"color:var(--error)\">type RESET to confirm</span>",
        );
        return;
    }
    wasm_bindgen_futures::spawn_local(async move {
        let fs = crate::app::shared_opfs();
        if let Ok(entries) = fs.read_dir("").await {
            for entry in entries {
                // Identity-preserving reset: KEEP the seed (`.lh_wallet`) and the
                // proven-owner hint (`.lh_owner`) so the device re-verifies on
                // reload instead of bricking. Everything else — history, app.rl,
                // cached keys, working files — is cleared. The seed is the ONLY
                // key to on-chain ownership; a local-only delete with no backup
                // was the brick (mobile especially: no signer-iframe path back).
                if entry.name == ".lh_wallet" || entry.name == ".lh_owner" {
                    continue;
                }
                let _ = fs.delete(&entry.name).await;
            }
        }
        if let Ok(window) = dom::window() {
            let _ = window.location().reload();
        }
    });
}

/// Parse the pricing-input as a decimal test-ETH amount, convert to
/// wei, persist via `pricing::save`, and re-paint the card so the
/// new value shows. Owner-only — the input is only rendered when
/// the verifier confirmed this visitor is the owner — but we still
/// re-check `verify_state` here as belt-and-suspenders against a
/// stale DOM.
pub(super) fn pricing_save_pressed() {
    let Some(input) = dom::input_by_id("pricing-input") else {
        return;
    };
    let raw = input.value().trim().to_string();
    let wei = match parse_eth_to_wei(&raw) {
        Ok(w) => w,
        Err(err) => {
            dom::swap_inner(
                "pricing-msg",
                &dom::msg_span(dom::Msg::Error, &err.to_string()),
            );
            return;
        }
    };

    let is_owner = crate::app::APP.with(|cell| {
        matches!(cell.borrow().verify_state, crate::app::VerifyState::Verified { .. })
    });
    if !is_owner {
        dom::swap_inner(
            "pricing-msg",
            "<span style=\"color:var(--error)\">only the verified owner can change pricing</span>",
        );
        return;
    }

    dom::swap_inner(
        "pricing-msg",
        "<span style=\"color:var(--muted)\">saving…</span>",
    );
    wasm_bindgen_futures::spawn_local(async move {
        match crate::app::pricing::save(wei).await {
            Ok(()) => {
                crate::app::APP
                    .with(|cell| cell.borrow_mut().pricing_wei = Some(wei));
                let html = templates::pricing_card_body(wei, true).into_string();
                dom::swap_outer("pricing-body", &html);
            }
            Err(err) => {
                dom::swap_inner(
                    "pricing-msg",
                    &dom::msg_span(dom::Msg::Error, &format!("save failed: {err}")),
                );
            }
        }
    });
}

/// Parse a decimal test-ETH amount ("0", "0.001", "1.5") into a wei
/// `u128`. Rejects negatives, NaN-shaped input, and values with more
/// than 18 fractional digits (wei is the precision floor).
fn parse_eth_to_wei(s: &str) -> Result<u128, String> {
    if s.is_empty() {
        return Ok(0);
    }
    let (whole_str, frac_str) = match s.split_once('.') {
        Some((w, f)) => (w, f),
        None => (s, ""),
    };
    if !whole_str.bytes().all(|b| b.is_ascii_digit()) {
        return Err("price must be a positive decimal".into());
    }
    if !frac_str.bytes().all(|b| b.is_ascii_digit()) {
        return Err("price must be a positive decimal".into());
    }
    if frac_str.len() > 18 {
        return Err("price has more precision than wei (18 decimals max)".into());
    }
    let whole: u128 = whole_str.parse().map_err(|e| format!("whole: {e}"))?;
    // Right-pad fraction to 18 digits then parse.
    let mut padded = String::with_capacity(18);
    padded.push_str(frac_str);
    while padded.len() < 18 {
        padded.push('0');
    }
    let frac: u128 = if padded.is_empty() {
        0
    } else {
        padded.parse().map_err(|e| format!("frac: {e}"))?
    };
    whole
        .checked_mul(1_000_000_000_000_000_000)
        .and_then(|w| w.checked_add(frac))
        .ok_or_else(|| "price too large".into())
}

// --- Action handlers ---------------------------------------------------

pub(super) fn clear_key_pressed() {
    if let Some(input) = dom::input_by_id("key") {
        input.set_value("");
    }
    if let Ok(Some(storage)) = dom::session_storage() {
        let _ = storage.remove_item("gemini_api_key");
    }
    super::refresh_keymeta();
    if let Some(input) = dom::input_by_id("key") {
        input.focus().ok();
    }
    wasm_bindgen_futures::spawn_local(async move {
        crate::app::key_store::clear().await;
    });
    dom::set_status("key cleared (sessionStorage + OPFS)", false);
}