use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
use crate::filesystem::OpfsFilesystem;
use crate::Agent;
mod chat;
mod compose;
mod dom;
mod embed;
mod events;
mod history;
mod key_store;
mod opfs;
mod owner;
mod pricing;
mod signer;
mod sponsor;
mod agent_rpc;
mod encryption;
mod system_prompt;
mod templates;
mod tool_allowlist;
mod tenant;
mod verify;
mod wallet_store;
pub(crate) use crate::registry;
pub(crate) struct App {
pub(crate) agent: Option<Rc<Agent>>,
pub(crate) session_key: Option<String>,
pub(crate) turn_count: u32,
pub(crate) next_id: u32,
pub(crate) opfs_cwd: Vec<String>,
pub(crate) opfs: Option<Arc<OpfsFilesystem>>,
pub(crate) pending_history: Option<Vec<u8>>,
pub(crate) wallet: Option<wallet_store::MasterWallet>,
pub(crate) verify_state: VerifyState,
pub(crate) tba_address: Option<String>,
pub(crate) pricing_wei: Option<u128>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) enum VerifyState {
#[default]
Pending,
Verified {
address: String,
},
Visitor {
owner_address: String,
visitor_address: String,
},
Unregistered,
Failed {
reason: String,
},
}
impl App {
fn new() -> Self {
Self {
agent: None,
session_key: None,
turn_count: 0,
next_id: 0,
opfs_cwd: Vec::new(),
opfs: None,
pending_history: None,
wallet: None,
verify_state: VerifyState::Pending,
tba_address: None,
pricing_wei: None,
}
}
pub(crate) fn alloc_id(&mut self) -> u32 {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
id
}
}
thread_local! {
pub(crate) static APP: RefCell<App> = RefCell::new(App::new());
}
pub(crate) fn shared_opfs() -> Arc<OpfsFilesystem> {
APP.with(|cell| {
let mut app = cell.borrow_mut();
if app.opfs.is_none() {
app.opfs = Some(Arc::new(OpfsFilesystem::new()));
}
app.opfs.as_ref().unwrap().clone()
})
}
#[wasm_bindgen(start)]
fn start() {
console_error_panic_hook::set_once();
if let Err(err) = mount() {
web_sys::console::error_1(&JsValue::from_str(&format!(
"localharness app failed to mount: {err:?}"
)));
}
}
fn mount() -> Result<(), JsValue> {
let doc = dom::document()?;
let root = doc
.get_element_by_id("root")
.ok_or_else(|| JsValue::from_str("missing <div id=\"root\"> in the host page"))?;
let host = tenant::current();
let host_for_listeners = host.clone();
events::install_delegated_listeners(&doc)?;
if let Some(names) = compose::compose_names() {
compose::paint_compose(names)?;
return Ok(());
}
if embed::has_embed_hint() {
root.set_inner_html(
"<main style=\"padding:24px;color:#7a8493;font:14px ui-monospace,Menlo,Consolas,monospace\">embed · loading…</main>",
);
let host_for_embed = host.clone();
wasm_bindgen_futures::spawn_local(async move {
embed::paint_embed(host_for_embed).await;
});
return Ok(());
}
if matches!(&host, tenant::Host::Apex) && has_signer_hint() {
root.set_inner_html("<main style=\"padding:48px;text-align:center;color:#7a8493;font:14px ui-monospace,Menlo,Consolas,monospace\">localharness signer · loading…</main>");
signer::install_signer_listener()?;
wasm_bindgen_futures::spawn_local(async move {
paint_signer().await;
});
return Ok(());
}
if agent_rpc::has_rpc_hint() {
root.set_inner_html("<main style=\"padding:24px;color:#7a8493;font:14px ui-monospace,Menlo,Consolas,monospace\">rpc · loading…</main>");
agent_rpc::install_rpc_listener()?;
wasm_bindgen_futures::spawn_local(async move {
agent_rpc::paint_rpc().await;
});
return Ok(());
}
match &host {
tenant::Host::Apex => {
let host_for_apex = host.clone();
root.set_inner_html(
"<main style=\"padding:48px;text-align:center;color:#7a8493;font:14px ui-monospace,Menlo,Consolas,monospace\">localharness · loading…</main>",
);
wasm_bindgen_futures::spawn_local(async move {
paint_apex(host_for_apex).await;
});
return Ok(());
}
tenant::Host::Tenant(name) => {
let placeholder = format!(
"<main style=\"padding:48px;text-align:center;color:#7a8493;\
font:14px ui-monospace,Menlo,Consolas,monospace\">\
resolving {name}…</main>"
);
root.set_inner_html(&placeholder);
let name = name.clone();
wasm_bindgen_futures::spawn_local(async move {
paint_tenant(host_for_listeners, name).await;
});
return Ok(());
}
tenant::Host::Other(_) => {
}
}
root.set_inner_html(&templates::chrome(&host).into_string());
wasm_bindgen_futures::spawn_local(async move {
let has_key = if let Some(persisted_key) = key_store::load().await {
if let Ok(Some(storage)) = dom::session_storage() {
let _ = storage.set_item("gemini_api_key", &persisted_key);
}
true
} else if let Ok(Some(storage)) = dom::session_storage() {
storage.get_item("gemini_api_key").ok().flatten().is_some()
} else {
false
};
history::load_into_pending().await;
opfs::refresh().await;
if !has_key {
show_api_key_modal();
}
});
Ok(())
}
pub(crate) async fn paint_tenant(host: tenant::Host, name: String) {
let Ok(doc) = dom::document() else { return };
let Some(root) = doc.get_element_by_id("root") else { return };
let mut owner = owner::current_owner().await;
if owner.is_none() && has_claim_hint() {
if let Ok(id) = owner::claim().await {
owner = Some(id);
strip_claim_hint();
}
}
if owner.is_none() {
let on_chain = registry::owner_of_name(&name).await.ok().flatten();
if on_chain.is_none() {
root.set_inner_html(&templates::unclaimed(&host, &name).into_string());
return;
}
}
root.set_inner_html(&templates::chrome(&host).into_string());
let has_key = if let Some(persisted_key) = key_store::load().await {
if let Ok(Some(storage)) = dom::session_storage() {
let _ = storage.set_item("gemini_api_key", &persisted_key);
}
true
} else if let Ok(Some(storage)) = dom::session_storage() {
storage.get_item("gemini_api_key").ok().flatten().is_some()
} else {
false
};
history::load_into_pending().await;
opfs::refresh().await;
if !has_key {
show_api_key_modal();
}
wasm_bindgen_futures::spawn_local(async move {
kick_verification(name).await;
});
}
async fn kick_verification(name: String) {
let outcome = match verify::verify_owner(&name).await {
Ok(verify::VerifyResult::VerifiedOwner { address }) => {
VerifyState::Verified { address }
}
Ok(verify::VerifyResult::Visitor {
owner_address,
visitor_address,
}) => VerifyState::Visitor {
owner_address,
visitor_address,
},
Ok(verify::VerifyResult::Unregistered) => VerifyState::Unregistered,
Err(err) => VerifyState::Failed { reason: err },
};
APP.with(|cell| cell.borrow_mut().verify_state = outcome.clone());
let html = templates::verify_pill(&outcome).into_string();
dom::swap_outer("verify-pill", &html);
if let VerifyState::Failed { reason } = &outcome {
dom::set_status(&format!("verify failed: {reason}"), true);
web_sys::console::warn_1(&JsValue::from_str(&format!(
"lh verify_owner failed: {reason}"
)));
}
let price = pricing::load().await.unwrap_or(0);
APP.with(|cell| cell.borrow_mut().pricing_wei = Some(price));
let is_owner = matches!(outcome, VerifyState::Verified { .. });
let on_chain = matches!(
outcome,
VerifyState::Verified { .. } | VerifyState::Visitor { .. }
);
let owner_addr: Option<String> = match &outcome {
VerifyState::Verified { address } => Some(address.clone()),
VerifyState::Visitor { owner_address, .. } => Some(owner_address.clone()),
_ => None,
};
let mut tba_opt: Option<String> = None;
if on_chain {
if let Ok(Some(tba)) = registry::tba_of_name(&name).await {
APP.with(|cell| cell.borrow_mut().tba_address = Some(tba.clone()));
tba_opt = Some(tba);
}
}
if let (Some(tba), Some(owner)) = (&tba_opt, &owner_addr) {
let lh_balance = registry::token_balance_of(tba).await.unwrap_or(0);
let html =
templates::financial_card(&name, tba, owner, lh_balance, price, is_owner)
.into_string();
dom::swap_outer("financial-slot", &html);
} else {
dom::swap_outer(
"financial-slot",
r#"<div id="financial-slot" class="financial-empty"></div>"#,
);
}
}
pub(crate) async fn paint_apex(host: tenant::Host) {
let Ok(doc) = dom::document() else { return };
let Some(root) = doc.get_element_by_id("root") else { return };
let wallet = wallet_store::load().await;
let addr_hex = wallet.as_ref().map(|w| w.address_hex());
APP.with(|cell| cell.borrow_mut().wallet = wallet);
root.set_inner_html(
&templates::apex(&host, addr_hex.as_deref()).into_string(),
);
if let Some(prefill) = read_query_param("prefill") {
let cleaned = tenant::sanitize(&prefill);
if !cleaned.is_empty() {
if let Some(input) = dom::input_by_id("apex-input") {
input.set_value(&cleaned);
if let Ok(event) = web_sys::Event::new("input") {
let _ = input.dispatch_event(&event);
}
let _ = input.focus();
}
}
}
if let Some(owner_addr) = addr_hex {
wasm_bindgen_futures::spawn_local(async move {
match registry::list_owned_tokens(&owner_addr).await {
Ok(agents) => {
let main_id = registry::main_of(&owner_addr).await.unwrap_or(0);
let html = templates::agents_list(&agents, main_id).into_string();
dom::swap_outer("agents-list", &html);
}
Err(err) => {
dom::swap_outer(
"agents-list",
&format!(
r#"<div id="agents-list" class="agents-list"><p class="apex-fine" style="color:var(--error)">couldn't list agents: {err}</p></div>"#
),
);
}
}
});
}
}
pub(crate) async fn paint_signer() {
let Ok(doc) = dom::document() else { return };
let Some(root) = doc.get_element_by_id("root") else { return };
match wallet_store::load().await {
Some(wallet) => {
let addr = wallet.address_hex();
APP.with(|cell| cell.borrow_mut().wallet = Some(wallet));
root.set_inner_html(&templates::signer_chrome(&addr).into_string());
}
None => {
root.set_inner_html(&templates::signer_no_identity().into_string());
}
}
if let Ok(window) = dom::window() {
if let Ok(Some(parent)) = window.parent() {
let ready = js_sys::Object::new();
let _ = js_sys::Reflect::set(
&ready,
&JsValue::from_str("type"),
&JsValue::from_str("lh-signer-ready"),
);
let _ = parent.post_message(&ready.into(), "*");
}
}
}
pub(crate) fn show_api_key_modal() {
let Ok(doc) = dom::document() else { return };
if doc.get_element_by_id("api-key-modal").is_some() {
return;
}
let Some(body) = doc.body() else { return };
let _ = body.insert_adjacent_html(
"beforeend",
&templates::api_key_modal().into_string(),
);
if let Some(input) = dom::input_by_id("api-key-input") {
let _ = input.focus();
}
}
pub(crate) fn format_wei_as_test_eth(wei: u128) -> String {
const WEI_PER_ETH: u128 = 1_000_000_000_000_000_000;
let whole = wei / WEI_PER_ETH;
let frac_wei = wei % WEI_PER_ETH;
if frac_wei == 0 {
return whole.to_string();
}
let frac = (frac_wei * 1_000_000) / WEI_PER_ETH;
format!("{whole}.{:06}", frac)
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}
fn read_query_param(key: &str) -> Option<String> {
let window = dom::window().ok()?;
let search = window.location().search().ok()?;
let stripped = search.trim_start_matches('?');
for pair in stripped.split('&') {
if let Some((k, v)) = pair.split_once('=') {
if k == key && !v.is_empty() {
return Some(decode_uri_component(v));
}
}
}
None
}
pub(crate) fn decode_uri_component(s: &str) -> String {
js_sys::decode_uri_component(s)
.map(|js| js.as_string().unwrap_or_else(|| s.to_string()))
.unwrap_or_else(|_| s.to_string())
}
fn has_signer_hint() -> bool {
let Ok(window) = dom::window() else { return false };
let Ok(search) = window.location().search() else { return false };
search.contains("signer=1")
}
fn has_claim_hint() -> bool {
let Ok(window) = dom::window() else { return false };
let Ok(search) = window.location().search() else { return false };
search.contains("claim=1") || search.contains("claim=true")
}
fn strip_claim_hint() {
let Ok(window) = dom::window() else { return };
let Ok(history) = window.history() else { return };
let url = window.location().pathname().unwrap_or_else(|_| "/".into());
let _ = history.replace_state_with_url(&JsValue::NULL, "", Some(&url));
}