use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
use crate::filesystem::{EncryptedFilesystem, OpfsFilesystem, SharedFilesystem};
use crate::Agent;
mod chat;
mod compose;
mod debuglog;
pub(crate) mod display;
pub(crate) mod agent_config;
mod dom;
mod embed;
mod events;
mod feedback;
mod gas;
mod history;
mod key_store;
mod lessons;
mod model;
mod net;
mod notifications;
mod opfs;
mod owner;
mod pricing;
mod remote_call;
mod seed_pull;
#[allow(dead_code)]
mod shared_fs;
#[allow(dead_code)]
mod webrtc;
#[allow(dead_code)]
mod sharedfs_sync;
#[allow(dead_code)]
mod teams_sync;
mod self_docs;
mod signer;
mod signer_protocol;
mod sponsor;
mod style;
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) opfs_at_rest: Option<SharedFilesystem>,
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>,
pub(crate) financial_card_html: Option<String>,
}
#[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,
opfs_at_rest: None,
pending_history: None,
wallet: None,
verify_state: VerifyState::Pending,
tba_address: None,
pricing_wei: None,
financial_card_html: 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() -> SharedFilesystem {
APP.with(|cell| {
let mut app = cell.borrow_mut();
if let Some(enc) = app.opfs_at_rest.as_ref() {
return enc.clone();
}
if app.opfs.is_none() {
app.opfs = Some(Arc::new(OpfsFilesystem::new()));
}
let raw: SharedFilesystem = app.opfs.as_ref().unwrap().clone();
raw
})
}
pub(crate) fn install_at_rest_encryption(key: [u8; 32]) {
APP.with(|cell| {
let mut app = cell.borrow_mut();
if app.opfs_at_rest.is_some() {
return;
}
if app.opfs.is_none() {
app.opfs = Some(Arc::new(OpfsFilesystem::new()));
}
let raw: SharedFilesystem = app.opfs.as_ref().unwrap().clone();
app.opfs_at_rest = Some(Arc::new(EncryptedFilesystem::new(raw, &key)));
});
}
#[wasm_bindgen(start)]
fn start() {
console_error_panic_hook::set_once();
debuglog::install_panic_banner();
if let Err(err) = mount() {
web_sys::console::error_1(&JsValue::from_str(&format!(
"localharness app failed to mount: {err:?}"
)));
}
}
#[wasm_bindgen]
pub fn push_arrived(title: String, body: String) {
notifications::push_arrived(&title, &body);
}
fn inject_token_styles(doc: &web_sys::Document) {
if doc.get_element_by_id("lh-tokens").is_some() {
return;
}
let Some(parent) = doc
.query_selector("head")
.ok()
.flatten()
.or_else(|| doc.document_element())
else {
return;
};
if let Ok(style_el) = doc.create_element("style") {
let _ = style_el.set_attribute("id", "lh-tokens");
style_el.set_text_content(Some(&style::root_tokens_css()));
let _ = parent.append_child(&style_el);
}
}
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"))?;
inject_token_styles(&doc);
let host = tenant::current();
let host_for_listeners = host.clone();
events::install_delegated_listeners(&doc)?;
crate::x402_hook::install(std::rc::Rc::new(|ch: crate::x402_hook::X402Challenge|
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<crate::x402_hook::X402Payment, String>>>> {
Box::pin(async move {
let (signer, from) = chat::credit_signer()
.await
.ok_or_else(|| "no identity to pay from".to_string())?;
let sig = crate::registry::sign_x402(
&signer,
&from,
&ch.to,
ch.value_wei,
ch.valid_after,
ch.valid_before,
&ch.nonce,
)?;
Ok(crate::x402_hook::X402Payment { from, signature: sig })
})
}));
crate::x402_hook::install_remote_call(std::rc::Rc::new(|target: String, message: String|
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>>>> {
Box::pin(async move { remote_call::ask_via_proxy(&target, &message).await })
}));
if let Some(names) = compose::compose_names() {
root.set_inner_html(&templates::app_fullscreen(false).into_string());
wasm_bindgen_futures::spawn_local(async move {
if let Err(err) = display::mount_composition(names).await {
web_sys::console::warn_1(&JsValue::from_str(&format!("compose failed: {err:?}")));
}
});
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 has_explore_hint() {
let host_for_explore = host.clone();
root.set_inner_html(&templates::explore_chrome(&host_for_explore).into_string());
dom::mark_ready();
wasm_bindgen_futures::spawn_local(async move {
paint_explore().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(());
}
capture_invite_code();
match &host {
tenant::Host::Apex => {
if let Some(owner) = read_query_param("link_device") {
root.set_inner_html(
"<main style=\"padding:48px;text-align:center;color:#7a8493;font:14px ui-monospace,Menlo,Consolas,monospace\">linking…</main>",
);
wasm_bindgen_futures::spawn_local(async move {
let _ = wallet_store::persist_linked_owner(&owner).await;
if let Some(then) = read_query_param("then") {
let valid_label = !then.is_empty()
&& then.len() <= 63
&& then.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
if valid_label {
if let Some(window) = web_sys::window() {
let _ = window
.location()
.set_href(&format!("https://{then}.localharness.xyz/"));
return;
}
}
}
paint_apex(tenant::Host::Apex).await;
});
return Ok(());
}
if read_query_param("seed_export").is_some() {
root.set_inner_html(
"<main style=\"padding:48px;text-align:center;color:#7a8493;font:14px ui-monospace,Menlo,Consolas,monospace\">linking this device…</main>",
);
wasm_bindgen_futures::spawn_local(async move {
seed_pull::handle_apex_export().await;
});
return Ok(());
}
if read_query_param("adopt").is_some() {
let ct_hex = read_fragment_param("s").unwrap_or_default();
root.set_inner_html(&templates::adopt_join(&ct_hex).into_string());
dom::mark_ready();
return Ok(());
}
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) => {
if read_query_param("seed_import").is_some() {
root.set_inner_html(
"<main style=\"padding:48px;text-align:center;color:#7a8493;\
font:14px ui-monospace,Menlo,Consolas,monospace\">\
setting up this device…</main>",
);
let name = name.clone();
let host_for_import = host_for_listeners.clone();
wasm_bindgen_futures::spawn_local(async move {
seed_pull::handle_tenant_import().await;
paint_tenant(host_for_import, name).await;
});
return Ok(());
}
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(
"<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 {
if try_paint_app(true).await {
return;
}
paint_workshop(&host).await;
});
Ok(())
}
async fn paint_workshop(host: &tenant::Host) {
let Ok(doc) = dom::document() else { return };
let Some(root) = doc.get_element_by_id("root") else { return };
root.set_inner_html(&templates::chrome(host).into_string());
dom::mark_ready();
wasm_bindgen_futures::spawn_local(events::try_redeem_pending_invite(true));
wasm_bindgen_futures::spawn_local(events::refresh_fund_banner());
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;
notifications::load_inbox().await;
opfs::refresh().await;
let _ = has_key;
}
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 };
wasm_bindgen_futures::spawn_local(events::try_redeem_pending_invite(true));
let local_wallet = wallet_store::load().await;
APP.with(|cell| cell.borrow_mut().wallet = local_wallet);
let mut owner = owner::current_owner().await;
if owner.is_none() && has_claim_hint() {
let registered = registry::owner_of_name(&name).await.ok().flatten();
if let Some(addr) = registered {
let _ = owner::remember(&addr).await;
owner = Some(addr);
strip_claim_hint();
}
}
let on_chain = if owner.is_none() {
registry::owner_of_name(&name).await.ok().flatten()
} else {
None
};
if owner.is_none() && on_chain.is_none() {
root.set_inner_html(&templates::unclaimed(&host, &name).into_string());
dom::mark_ready();
return;
}
let signer_owner = if owner.is_none() {
match chat::credit_address_existing().await {
Some(my_addr) => {
let mut ok = false;
if let Ok(Some(tba)) = registry::tba_of_name(&name).await {
ok = registry::is_authorized_signer(&tba, &my_addr)
.await
.unwrap_or(false);
}
if !ok {
if let Some(oc) = on_chain.as_deref() {
ok = registry::is_authorized_signer(oc, &my_addr)
.await
.unwrap_or(false);
}
}
ok
}
None => false,
}
} else {
false
};
let is_owner_device = owner.is_some() || signer_owner;
let show_public_face = !is_owner_device || has_view_public_hint();
if show_public_face {
paint_public_face(&host, &name, is_owner_device).await;
if !is_owner_device {
let n = name.clone();
wasm_bindgen_futures::spawn_local(async move {
redirect_to_studio_if_owner(n).await;
});
}
return;
}
if APP.with(|cell| cell.borrow().wallet.is_none())
&& seed_pull::maybe_auto_kick(&name).await
{
return; }
root.set_inner_html(&templates::chrome(&host).into_string());
dom::mark_ready();
wasm_bindgen_futures::spawn_local(events::refresh_fund_banner());
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;
notifications::load_inbox().await;
opfs::refresh().await;
if !has_key {
let _ = events::try_auto_restore_gemini_key(&name).await;
wasm_bindgen_futures::spawn_local(async {
notifications::refresh_subscription_if_stale().await;
});
wasm_bindgen_futures::spawn_local(async {
notifications::auto_register_device_push().await;
});
}
let painted_from_hint = owner.is_some();
let host_for_verify = host.clone();
wasm_bindgen_futures::spawn_local(async move {
kick_verification(host_for_verify, name, painted_from_hint).await;
});
}
async fn kick_verification(host: tenant::Host, name: String, painted_from_hint: bool) {
let verify_result = match net::with_timeout(verify::VERIFY_BUDGET_MS, verify::verify_owner(&name)).await {
Ok(r) => r,
Err(_) => Err("verification timed out (RPC unreachable)".to_string()),
};
match &verify_result {
Ok(verify::VerifyResult::VerifiedOwner { address }) => {
let _ = owner::remember(address).await;
}
Ok(verify::VerifyResult::Visitor { .. })
| Ok(verify::VerifyResult::Unregistered) => {
if painted_from_hint {
owner::forget().await;
paint_public_face(&host, &name, false).await;
return;
}
}
Err(_) => {}
}
let outcome = match verify_result {
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);
}
}
let card_html = if let (Some(tba), Some(owner)) = (&tba_opt, &owner_addr) {
let lh_balance = registry::token_balance_of(tba).await.unwrap_or(0);
templates::financial_card(&name, tba, owner, lh_balance, price, is_owner).into_string()
} else {
r#"<div id="financial-slot" class="financial-empty"></div>"#.to_string()
};
APP.with(|cell| cell.borrow_mut().financial_card_html = Some(card_html.clone()));
if dom::by_id("financial-slot").is_some() {
dom::swap_outer("financial-slot", &card_html);
}
}
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 mut addr_hex = wallet.as_ref().map(|w| w.address_hex());
if addr_hex.is_none() {
addr_hex = wallet_store::load_linked_owner().await;
}
APP.with(|cell| cell.borrow_mut().wallet = wallet);
wasm_bindgen_futures::spawn_local(events::try_redeem_pending_invite(false));
root.set_inner_html(
&templates::apex(&host, addr_hex.as_deref()).into_string(),
);
dom::mark_ready();
wasm_bindgen_futures::spawn_local(async move {
if wallet_store::storage_is_volatile().await {
dom::swap_inner(
"storage-warn-slot",
&templates::volatile_storage_warning().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 {
dom::swap_outer(
"agents-list",
r#"<div id="agents-list" class="agents-list"><p class="apex-fine">loading agents…</p></div>"#,
);
wasm_bindgen_futures::spawn_local(async move {
let listed = match net::read(registry::list_owned_tokens(&owner_addr)).await {
Ok(r) => r,
Err(_) => Err("on-chain read timed out".to_string()),
};
match listed {
Ok(mut agents) => {
let main_id = registry::main_of(&owner_addr).await.unwrap_or(0);
if main_id != 0 {
if let Some(pos) =
agents.iter().position(|a| a.token_id == main_id)
{
let main = agents.remove(pos);
agents.insert(0, main);
}
}
let html = templates::agents_list(&agents, main_id).into_string();
dom::swap_outer("agents-list", &html);
}
Err(err) => {
dom::swap_outer(
"agents-list",
&maud::html! {
div #agents-list .agents-list {
p .apex-fine style="color:var(--error)" {
"couldn't list agents: " (err)
}
}
}
.into_string(),
);
}
}
});
}
}
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(signer_protocol::MSG_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()
}
pub(crate) 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 read_fragment_param(key: &str) -> Option<String> {
let window = dom::window().ok()?;
let hash = window.location().hash().ok()?;
let stripped = hash.trim_start_matches('#');
for pair in stripped.split('&') {
if let Some((k, v)) = pair.split_once('=') {
if k == key && !v.is_empty() {
return Some(v.to_string());
}
}
}
None
}
fn capture_invite_code() {
let Some(code) = read_query_param("invite") else {
return;
};
if let Some(storage) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
let _ = storage.set_item("lh_pending_invite", &code);
}
if let Some(window) = web_sys::window() {
if let Ok(history) = window.history() {
let path = window
.location()
.pathname()
.unwrap_or_else(|_| "/".to_string());
let _ = history.replace_state_with_url(
&wasm_bindgen::JsValue::NULL,
"",
Some(&path),
);
}
}
}
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_explore_hint() -> bool {
let Ok(window) = dom::window() else { return false };
let Ok(search) = window.location().search() else { return false };
search.contains("explore=1")
}
async fn paint_explore() {
let listed = match net::read(registry::list_recent_agents(60)).await {
Ok(r) => r,
Err(_) => Err("on-chain read timed out".to_string()),
};
match listed {
Ok(agents) => {
let ids: Vec<u64> = agents.iter().map(|(id, _)| *id).collect();
let personas = registry::personas_of(&ids).await;
dom::swap_outer(
"explore-grid",
&templates::explore_grid(&agents, &personas).into_string(),
);
}
Err(err) => {
dom::swap_inner(
"explore-grid",
&maud::html! {
span style="color:var(--muted)" { "couldn't load agents: " (err) }
}
.into_string(),
);
}
}
}
fn has_edit_hint() -> bool {
let Ok(window) = dom::window() else { return false };
let Ok(search) = window.location().search() else { return false };
search.contains("edit=1")
}
async fn paint_public_landing(host: &tenant::Host, name: &str, owner_overlay: bool) {
let _ = host;
let owner = registry::owner_of_name(name).await.ok().flatten();
let tba = registry::tba_of_name(name).await.ok().flatten();
let mut main_name: Option<String> = None;
let mut is_main = false;
let mut siblings: Vec<registry::OwnedToken> = Vec::new();
if let Some(addr) = owner.as_deref() {
let main_id = registry::main_of(addr).await.unwrap_or(0);
if main_id != 0 {
if let Ok(m) = registry::name_of_id(main_id).await {
if !m.is_empty() {
is_main = m == name;
if !is_main {
main_name = Some(m);
}
}
}
}
siblings = registry::list_owned_tokens(addr).await.unwrap_or_default();
siblings.retain(|t| t.name != name);
}
let sibling_ids: Vec<u64> = siblings.iter().map(|t| t.token_id).collect();
let personas = registry::personas_of(&sibling_ids).await;
let html = templates::public_landing(
name,
owner.as_deref(),
tba.as_deref(),
main_name.as_deref(),
is_main,
&siblings,
&personas,
owner_overlay,
)
.into_string();
if let Ok(doc) = dom::document() {
if let Some(root) = doc.get_element_by_id("root") {
root.set_inner_html(&html);
}
}
}
async fn redirect_to_studio_if_owner(name: String) {
let verdict = match net::with_timeout(verify::VERIFY_BUDGET_MS, verify::verify_owner(&name)).await {
Ok(r) => r,
Err(_) => Err("verification timed out".to_string()),
};
match verdict {
Ok(verify::VerifyResult::VerifiedOwner { address }) => {
let _ = owner::remember(&address).await;
if let Ok(window) = dom::window() {
let _ = window.location().set_search("edit=1");
}
}
Err(_) => {
seed_pull::maybe_auto_kick(&name).await;
}
Ok(_) => {}
}
}
fn has_view_public_hint() -> bool {
let Ok(window) = dom::window() else { return false };
let Ok(search) = window.location().search() else { return false };
search.contains("view=public")
}
enum PublicFace {
Directory,
Cartridge(Vec<u8>),
Html(String),
}
async fn local_cartridge_wasm() -> Option<Vec<u8>> {
let fs = shared_opfs();
let bytes = fs.read("app.rl").await.ok().filter(|b| !b.is_empty())?;
let src = String::from_utf8_lossy(&bytes).into_owned();
match crate::rustlite::compile(&src) {
Ok(w) => Some(w),
Err(err) => {
web_sys::console::warn_1(&JsValue::from_str(&format!(
"app.rl compile failed, falling back: {err}"
)));
None
}
}
}
async fn local_public_html() -> Option<String> {
let fs = shared_opfs();
let bytes = fs.read("index.html").await.ok().filter(|b| !b.is_empty())?;
Some(String::from_utf8_lossy(&bytes).into_owned())
}
async fn compose_module_wasm(name: &str) -> Option<Vec<u8>> {
let id = registry::id_of_name(name).await.ok().filter(|&i| i != 0)?;
registry::app_wasm_of(id).await.ok().flatten()
}
async fn resolve_cartridge(id: Option<u64>, prefer_local: bool) -> Option<Vec<u8>> {
if prefer_local {
if let Some(w) = local_cartridge_wasm().await {
return Some(w);
}
}
match id {
Some(i) => registry::app_wasm_of(i).await.ok().flatten(),
None => None,
}
}
async fn resolve_public_face(name: &str, is_owner_preview: bool) -> PublicFace {
let id = registry::id_of_name(name).await.ok().filter(|&i| i != 0);
let choice = match id {
Some(i) => registry::public_face_of(i).await.ok().flatten(),
None => None,
};
match choice.as_deref() {
Some("directory") => PublicFace::Directory,
Some("html") => {
if is_owner_preview {
if let Some(h) = local_public_html().await {
return PublicFace::Html(h);
}
}
if let Some(i) = id {
if let Ok(Some(bytes)) = registry::public_html_of(i).await {
return PublicFace::Html(String::from_utf8_lossy(&bytes).into_owned());
}
}
PublicFace::Directory
}
_ => match resolve_cartridge(id, is_owner_preview).await {
Some(w) => PublicFace::Cartridge(w),
None => PublicFace::Directory,
},
}
}
async fn paint_cartridge_fullscreen(wasm: &[u8], owner_overlay: bool) -> bool {
let Ok(doc) = dom::document() else { return false };
let Some(root) = doc.get_element_by_id("root") else { return false };
root.set_inner_html(&templates::app_fullscreen(owner_overlay).into_string());
if let Err(err) = display::run_in_root_canvas(wasm).await {
web_sys::console::warn_1(&JsValue::from_str(&format!("app run failed: {err:?}")));
}
true
}
fn paint_html_fullscreen(html: &str, owner_overlay: bool) -> bool {
let Ok(doc) = dom::document() else { return false };
let Some(root) = doc.get_element_by_id("root") else { return false };
root.set_inner_html(&templates::app_fullscreen(owner_overlay).into_string());
if let Err(err) = display::render_html_in_root_canvas(html) {
web_sys::console::warn_1(&JsValue::from_str(&format!("html render failed: {err:?}")));
}
true
}
async fn paint_public_face(host: &tenant::Host, name: &str, owner_overlay: bool) {
match resolve_public_face(name, owner_overlay).await {
PublicFace::Cartridge(w) => {
paint_cartridge_fullscreen(&w, owner_overlay).await;
}
PublicFace::Html(h) => {
paint_html_fullscreen(&h, owner_overlay);
}
PublicFace::Directory => {
paint_public_landing(host, name, owner_overlay).await;
}
}
dom::mark_ready();
}
async fn try_paint_app(owner_overlay: bool) -> bool {
if has_edit_hint() {
return false;
}
let Some(wasm) = local_cartridge_wasm().await else {
return false;
};
paint_cartridge_fullscreen(&wasm, owner_overlay).await
}
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));
}