use maud::{html, Markup, PreEscaped};
use crate::filesystem::{DirEntry, EntryKind};
use crate::types::{BuiltinTool, ToolCall, ToolResult};
use super::tenant::Host;
use super::VerifyState;
pub(crate) fn api_key_modal() -> Markup {
html! {
div #api-key-modal .api-key-modal {
div.api-key-card {
div.api-key-title { "power this agent" }
button type="button" data-action="set-model-access" data-arg="credits"
.ghost.api-key-primary { "use platform credits" }
div.api-key-or { "or bring your own key" }
form onsubmit="return false" {
div.api-key-row {
input #api-key-input
type="password"
autocomplete="off"
placeholder="paste key" {}
button type="button"
data-action="save-api-key" { "save" }
}
}
div.api-key-hint {
a href="https://aistudio.google.com/apikey"
target="_blank" rel="noopener" { "get a free key โ" }
}
div #api-key-msg .feedback-msg {}
}
}
}
}
pub(crate) fn rendered_markdown(raw: &str) -> Markup {
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag};
fn safe_url(url: CowStr) -> CowStr {
let probe = url.trim_start().to_ascii_lowercase();
let dangerous = probe.starts_with("javascript:")
|| probe.starts_with("vbscript:")
|| probe.starts_with("data:");
if dangerous {
CowStr::Borrowed("#")
} else {
url
}
}
let mut opts = Options::empty();
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(raw, opts).map(|event| match event {
Event::Html(h) | Event::InlineHtml(h) => Event::Text(h),
Event::Start(Tag::Link { link_type, dest_url, title, id }) => Event::Start(Tag::Link {
link_type,
dest_url: safe_url(dest_url),
title,
id,
}),
Event::Start(Tag::Image { link_type, dest_url, title, id }) => Event::Start(Tag::Image {
link_type,
dest_url: safe_url(dest_url),
title,
id,
}),
other => other,
});
let mut out = String::with_capacity(raw.len());
html::push_html(&mut out, parser);
html! { (PreEscaped(out)) }
}
pub(crate) fn site_header(_host: &Host) -> Markup {
html! {
header.site-header {
div.header-inner {
h1.header-brand {
details.brand-menu {
summary.brand-summary { "localharness" }
nav.brand-menu-items {
a href="https://localharness.xyz/" { "home" }
a href="https://github.com/compusophy/localharness"
target="_blank" rel="noopener" { "repo" }
a href="https://crates.io/crates/localharness"
target="_blank" rel="noopener" { "crate" }
}
}
}
div #header-admin .header-admin {
button type="button"
data-action="header-admin-toggle"
.header-button.admin-button { "admin" }
div #header-admin-panel hidden {}
}
}
}
}
}
pub(crate) const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) fn terminal_input() -> Markup {
html! {
div.terminal-body {
div #fund-banner .fund-banner {}
div #status .terminal-status role="status" aria-live="polite" {}
div.terminal-row {
span.terminal-prompt aria-hidden="true" { ">" }
textarea #prompt rows="1" aria-label="message the agent" {}
(send_button())
}
}
}
}
pub(crate) fn send_button() -> Markup {
html! {
button #terminal-send .terminal-send data-action="send" title="send" aria-label="send" { "โถ" }
}
}
pub(crate) fn stop_button() -> Markup {
html! {
button #terminal-stop .terminal-send.terminal-stop data-action="stop-turn" title="stop" aria-label="stop generating" { "โ " }
}
}
pub(crate) fn fund_banner_body() -> Markup {
html! {
div style="display:flex;flex-wrap:wrap;align-items:center;gap:8px;\
padding:8px 10px;margin-bottom:8px;\
border:1px solid var(--border);background:var(--panel);\
font-size:12px;color:var(--muted)" {
span { "no $LH yet โ redeem a code to start" }
input #fund-redeem-code .redeem-input type="text" placeholder="redeem code";
button type="button" data-action="redeem-banner" .ghost { "redeem" }
div #fund-msg .admin-msg-slot style="margin-top:0;flex-basis:100%" {}
}
}
}
pub(crate) fn verify_pill(state: &VerifyState) -> Markup {
let (class, label, title) = match state {
VerifyState::Pending => (
"tag verify-pill verify-pending",
"verifyingโฆ".to_string(),
"checking ownership against the on-chain registry".to_string(),
),
VerifyState::Verified { address } => (
"tag verify-pill verify-ok",
"โ owner".to_string(),
format!("signature recovered {address} โ matches on-chain owner"),
),
VerifyState::Visitor { owner_address, .. } => (
"tag verify-pill verify-visitor",
format!("visitor ยท owner {}", short_addr(owner_address)),
format!("the on-chain owner of this name is {owner_address}"),
),
VerifyState::Unregistered => (
"tag verify-pill verify-unregistered",
"not on-chain".to_string(),
"this name isn't in the registry โ local-only".to_string(),
),
VerifyState::Failed { reason } => (
"tag verify-pill verify-failed",
"verify failed".to_string(),
format!("verification didn't complete: {reason}"),
),
};
html! {
span #verify-pill class=(class) title=(title) role="status" aria-label=(title) { (label) }
}
}
fn short_addr(addr: &str) -> String {
let stripped = addr.trim_start_matches("0x");
if stripped.len() < 8 {
return addr.to_string();
}
format!("0x{}โฆ{}", &stripped[..4], &stripped[stripped.len() - 4..])
}
fn truncate_preview(text: &str, max: usize) -> String {
let flat: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
if flat.chars().count() <= max {
return flat;
}
let cut: String = flat.chars().take(max).collect();
format!("{}โฆ", cut.trim_end())
}
pub(crate) fn embed_card(
name: &str,
owner_hex: Option<&str>,
tba_hex: Option<&str>,
lh_balance_wei: Option<u128>,
is_main: Option<bool>,
) -> Markup {
let lh_whole = lh_balance_wei.map(|w| w / 1_000_000_000_000_000_000u128);
html! {
section.embed-card {
div.embed-card-header {
a.embed-card-name
href=(format!("https://{name}.localharness.xyz/"))
target="_top"
rel="noopener" {
(name)
}
@if let Some(true) = is_main {
span.embed-card-badge { "main" }
}
}
div.embed-card-rows {
@if let Some(addr) = owner_hex {
div.embed-card-row {
span.embed-card-label { "owner" }
code.embed-card-value title=(addr) { (short_addr(addr)) }
}
} @else if owner_hex.is_some() {
} @else {
div.embed-card-row {
span.embed-card-label { "owner" }
code.embed-card-value.embed-card-muted { "โฆ" }
}
}
@if let Some(addr) = tba_hex {
div.embed-card-row {
span.embed-card-label { "wallet" }
code.embed-card-value title=(addr) { (short_addr(addr)) }
}
}
@if let Some(lh) = lh_whole {
div.embed-card-row {
span.embed-card-label { "balance" }
code.embed-card-value { (lh) " LH" }
}
}
}
}
}
}
pub(crate) fn explore_chrome(host: &Host) -> Markup {
html! {
(site_header(host))
main.explore-main {
div.explore-header {
h1.explore-title { "agents" }
}
div #explore-grid .explore-grid { "loadingโฆ" }
}
}
}
pub(crate) fn explore_grid(agents: &[(u64, String)], personas: &[Option<String>]) -> Markup {
if agents.is_empty() {
return html! {
div #explore-grid .explore-grid .explore-empty {
"no agents yet โ "
a href="https://localharness.xyz/" { "claim the first one" }
}
};
}
html! {
div #explore-grid .explore-grid {
@for (i, (_, name)) in agents.iter().enumerate() {
@let preview = personas.get(i).and_then(|p| p.as_deref());
a.explore-card
href=(format!("https://{name}.localharness.xyz/"))
rel="noopener" {
span.explore-card-name { (name) }
span.explore-card-host { (name) ".localharness.xyz" }
@if let Some(p) = preview {
span.explore-card-preview { (truncate_preview(p, 80)) }
}
}
}
}
}
}
pub(crate) fn chrome(host: &Host) -> Markup {
html! {
(site_header(host))
(mobile_tabs())
main #layout .layout.view-collapsed.files-collapsed.financial-collapsed.tab-chat {
button type="button" data-action="toggle-files"
.side-rail.files-rail {
span.rail-label { "files" }
}
(col_side(
html! {
div #fs-breadcrumb .fs-breadcrumb { "/" }
ul #fs-list .fs-list {}
},
"col-fs",
))
div.col-chat {
button type="button" data-action="toggle-display"
.top-rail.display-rail {
span.rail-label { "display" }
}
section.view-panel {
div #view-content .view-content {}
}
div #transcript .transcript role="log" aria-live="polite" aria-atomic="false"
aria-label="agent conversation" {}
section.terminal-panel {
(terminal_input())
}
button type="button" data-action="toggle-terminal"
.bottom-rail.terminal-rail {
span.rail-label { "terminal" }
}
}
}
}
}
pub(crate) fn mobile_tabs() -> Markup {
html! {
nav.mobile-tabs {
button #tab-btn-files type="button" data-action="show-tab" data-arg="files" .tab-button { "files" }
button #tab-btn-chat type="button" data-action="show-tab" data-arg="chat" .tab-button.active { "chat" }
button #tab-btn-display type="button" data-action="show-tab" data-arg="display" .tab-button { "display" }
}
}
}
pub(crate) fn admin_feedback_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "feedback" }
textarea #feedback-text
.feedback-textarea
rows="6" {}
div.prompt-actions {
button type="button" data-action="feedback-submit" .ghost { "submit" }
}
div #feedback-msg .feedback-msg .admin-msg-slot {}
}
}
}
fn col_side(body: Markup, extra_class: &str) -> Markup {
let cls = format!("col-side {extra_class}");
html! {
aside class=(cls) {
div.panel-body { (body) }
}
}
}
pub(crate) fn turn(turn_id: u32, role: &str, body: Markup, streaming: bool) -> Markup {
let role_class = role; let id_str = format!("turn-{turn_id}");
let body_id = format!("turn-body-{turn_id}");
let cls = if streaming {
format!("turn {role_class} streaming")
} else {
format!("turn {role_class}")
};
html! {
div id=(id_str) class=(cls) {
div id=(body_id) .body { (body) }
}
}
}
pub(crate) fn text_segment(seg_id: u32, text: &str) -> Markup {
let id_str = format!("seg-{seg_id}");
html! {
div id=(id_str) .text-segment { (text) }
}
}
pub(crate) fn tool_call_block(seg_id: u32, call: &ToolCall) -> Markup {
let block_id = format!("tool-{seg_id}");
let result_id = format!("tool-{seg_id}-result");
let args_pretty = serde_json::to_string_pretty(&call.args).unwrap_or_else(|_| "{}".into());
html! {
details id=(block_id) .tool-call {
summary {
span.tc-name { (call.name) }
}
div.tc-body {
div.tc-section-label { "args" }
pre { (args_pretty) }
div id=(result_id) {}
}
}
}
}
pub(crate) fn tool_call_result(result: &ToolResult) -> Markup {
let ok = result.error.is_none();
html! {
div.tc-section-label { (if ok { "result" } else { "error" }) }
@if ok {
pre {
(match &result.result {
Some(v) => serde_json::to_string_pretty(v).unwrap_or_else(|_| "(unserializable)".into()),
None => "(no output)".into(),
})
}
} @else {
div.tc-error {
pre { (result.error.as_deref().unwrap_or("(unknown error)")) }
}
}
}
}
pub(crate) fn apex(host: &Host, _wallet_address_hex: Option<&str>) -> Markup {
html! {
(site_header(host))
main.apex-main {
div.col-chat {
(apex_claim())
div.apex-explore-link {
a href="?explore=1" { "explore all agents โ" }
}
div.apex-explore-link {
a href="/skill.md" { "for agents: how to join โ" }
}
}
}
}
}
fn apex_claim() -> Markup {
html! {
section.step.step-agents {
div #agents-list .agents-list {}
form.create-form data-action="apex-claim" {
input #apex-input
.create-input
type="text"
placeholder="choose a name"
autocomplete="off"
spellcheck="false"
maxlength="32"
required {}
button #create-btn type="submit" .create-button disabled { "create" }
}
}
}
}
pub(crate) fn admin_dropdown_apex() -> Markup {
let owner_hex = super::APP.with(|cell| {
cell.borrow().wallet.as_ref().map(|w| w.address_hex())
});
let has_wallet = owner_hex.is_some();
html! {
div #header-admin-panel .header-admin-panel {
div #admin-dialog .admin-dialog.admin-tabbed.tab-account {
div.admin-tabs {
button #admin-tab-btn-account type="button"
data-action="show-admin-tab" data-arg="account"
.admin-tab-button.active { "account" }
button #admin-tab-btn-usage type="button"
data-action="show-admin-tab" data-arg="usage"
.admin-tab-button { "usage" }
button #admin-tab-btn-feedback type="button"
data-action="show-admin-tab" data-arg="feedback"
.admin-tab-button { "feedback" }
span.admin-tabs-spacer {}
button type="button" data-action="header-admin-close" .modal-close aria-label="close admin" { "ร" }
}
div.admin-tab-panel.panel-feedback {
(admin_feedback_section())
}
div.admin-tab-panel.panel-account {
(admin_identity_section(None, owner_hex.as_deref(), None, has_wallet))
@if has_wallet {
(admin_devices_section())
}
(admin_security_collapsed())
}
div.admin-tab-panel.panel-usage {
@if has_wallet { (admin_credits_section()) }
(admin_usage_section())
}
div.admin-footer {
span.admin-version { (APP_VERSION) }
}
}
}
}
}
pub(crate) fn admin_dropdown_tenant() -> Markup {
html! {
div #header-admin-panel .header-admin-panel {
div #admin-dialog .admin-dialog.admin-tabbed.tab-account {
div.admin-tabs {
button #admin-tab-btn-agent type="button"
data-action="show-admin-tab" data-arg="agent"
.admin-tab-button { "agent" }
button #admin-tab-btn-account type="button"
data-action="show-admin-tab" data-arg="account"
.admin-tab-button.active { "account" }
button #admin-tab-btn-usage type="button"
data-action="show-admin-tab" data-arg="usage"
.admin-tab-button { "usage" }
button #admin-tab-btn-feedback type="button"
data-action="show-admin-tab" data-arg="feedback"
.admin-tab-button { "feedback" }
span.admin-tabs-spacer {}
button type="button" data-action="header-admin-close" .modal-close aria-label="close admin" { "ร" }
}
div.admin-tab-panel.panel-feedback {
(admin_feedback_section())
}
div.admin-tab-panel.panel-agent {
(admin_model_section())
(admin_prompt_section())
(admin_x402_price_section())
(admin_tool_allowlist_section())
(admin_app_section())
}
div.admin-tab-panel.panel-account {
div #financial-slot .financial-placeholder { "โ" }
(admin_credits_section())
(admin_security_collapsed())
}
div.admin-tab-panel.panel-usage {
(admin_usage_section())
}
div.admin-footer {
span.admin-version { (APP_VERSION) }
}
}
}
}
}
pub(crate) fn admin_usage_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "usage" }
div.admin-identity-row {
span.admin-identity-label { "subdomains" }
code #usage-subdomains .admin-identity-value { "โฆ" }
}
div.admin-identity-row {
span.admin-identity-label { "tokens (session)" }
code #usage-tokens .admin-identity-value { "0" }
}
}
}
}
pub(crate) fn admin_prompt_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "agent prompt" }
form.prompt-form data-action="save-prompt" onsubmit="return false" {
textarea #prompt-input
.prompt-input
rows="5"
placeholder="optional โ empty uses the default" {}
div.prompt-actions {
button type="submit" .ghost { "save" }
}
}
div #prompt-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_model_section() -> Markup {
html! {
div #model-section .admin-section {
div.admin-section-title { "model" }
div #model-selector-row .public-face-picker {
@for (id, label) in super::model::MODELS {
button type="button" data-action="set-model" data-arg=(id)
class="ghost" data-model=(id) { (label) }
}
}
div #model-msg .admin-msg-slot {}
div.public-face-preview {
button type="button" data-action="download-local-model" .ghost {
"download local model"
}
}
div #local-model-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_x402_price_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "x402 price" }
form.prompt-form data-action="save-x402-price" onsubmit="return false" {
input #x402-price-input .redeem-input type="text" placeholder="price per call (LH)";
div.prompt-actions {
button type="submit" .ghost { "save" }
}
}
div #x402-price-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_app_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "public face" }
div #public-face-status .admin-msg-slot { "what visitors see at this subdomain" }
div.public-face-picker {
button type="button" data-action="set-public-face" data-arg="directory" .ghost { "directory" }
button type="button" data-action="set-public-face" data-arg="app" .ghost { "publish app" }
button type="button" data-action="set-public-face" data-arg="html" .ghost { "publish html" }
}
div #publish-app-msg .admin-msg-slot {}
div.public-face-preview {
a href="?view=public" { "view public face โ" }
}
}
}
}
pub(crate) fn admin_tool_allowlist_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "tool allowlist" }
div #tool-allowlist-status .admin-msg-slot { "loadingโฆ" }
div.tool-allowlist-grid {
@for tool in BuiltinTool::ALL {
label.tool-checkbox-label {
input.tool-checkbox
type="checkbox"
data-tool=(tool.wire_name())
checked {}
" " (tool.wire_name())
}
}
}
div.prompt-actions {
button type="button"
data-action="save-tool-allowlist"
.ghost { "save" }
button type="button"
data-action="reset-tool-allowlist"
.ghost { "reset (all)" }
}
div #tool-allowlist-msg .admin-msg-slot {}
}
}
}
fn admin_identity_section(
name: Option<&str>,
owner_hex: Option<&str>,
tba_hex: Option<&str>,
has_wallet: bool,
) -> Markup {
html! {
div.admin-section {
@if let Some(n) = name {
div.admin-identity-row {
span.admin-identity-label { "name" }
code.admin-identity-value { (n) }
}
}
@if let Some(addr) = owner_hex {
div.admin-identity-row {
span.admin-identity-label { "owner" }
a.admin-identity-value
href=(format!("https://moderato.tempo.xyz/address/{addr}"))
target="_blank" rel="noopener"
title=(addr) {
(short_addr(addr))
}
}
} @else if has_wallet {
p.admin-blurb { "verifyingโฆ" }
} @else {
p.admin-blurb { "no identity on this device" }
div.pair-slot {
button type="button" data-action="create-identity" .ghost {
"create a new identity"
}
}
div.pair-slot {
button type="button" data-action="show-import" .ghost {
"i already have one โ import seed"
}
}
div #import-slot {}
div #identity-msg .admin-msg-slot {}
div #seed-msg .admin-msg-slot {}
p.admin-blurb {
"on mobile? "
a href="https://localharness.xyz/?adopt=1" target="_top" rel="noopener" {
"restore from your seed โ"
}
}
}
@if let Some(addr) = tba_hex {
div.admin-identity-row {
span.admin-identity-label { "wallet" }
a.admin-identity-value
href=(format!("https://moderato.tempo.xyz/address/{addr}"))
target="_blank" rel="noopener"
title=(addr) {
(short_addr(addr))
}
}
}
}
}
}
pub(crate) fn admin_credits_section() -> Markup {
html! {
div #credits-section .admin-section {
div.admin-section-title { "credits" }
div.admin-credits-row {
code #credits-balance .admin-identity-value { "โฆ" }
}
div.redeem-row {
input #redeem-code .redeem-input type="text" placeholder="redeem code";
button type="button" data-action="redeem-code" .ghost { "redeem" }
}
div #credits-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_devices_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "devices" }
div #pair-slot .pair-slot {
button #pair-btn type="button" data-action="add-device" .ghost {
"add a device"
}
}
div.pair-slot {
button type="button" data-action="sync-devices" .ghost {
"sync my devices"
}
}
div #pair-msg .admin-msg-slot {}
}
}
}
fn pair_qr_svg(pair_url: &str) -> Option<String> {
use qrcode::render::svg;
use qrcode::QrCode;
let code = QrCode::new(pair_url.as_bytes()).ok()?;
Some(
code.render::<svg::Color>()
.min_dimensions(200, 200)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.quiet_zone(true)
.build(),
)
}
pub(crate) fn pair_panel(code: &str, pair_url: &str) -> Markup {
html! {
div #pair-slot .pair-slot.pair-active {
div.pair-instructions {
"scan with your phone's camera, or open:"
}
@if let Some(svg) = pair_qr_svg(pair_url) {
div.pair-qr { (PreEscaped(svg)) }
}
a.pair-url href=(pair_url) target="_blank" rel="noopener" { (pair_url) }
div.pair-code-row {
span.pair-code-label { "code" }
code.pair-code { (code) }
}
div.pair-waiting { "waiting for the other deviceโฆ" }
button type="button" data-action="pair-cancel" .ghost { "cancel" }
}
}
}
pub(crate) fn pair_confirm_panel(device: &str) -> Markup {
html! {
div #pair-slot .pair-slot.pair-active {
div.pair-instructions { "a device wants to link as this identity:" }
div.pair-code-row {
span.pair-code-label { "device" }
code.pair-code { (device) }
}
div.pair-waiting {
"only approve if this address matches the one shown on the \
device you're pairing."
}
div.pair-confirm-actions {
button type="button" data-action="pair-reject" .ghost { "reject" }
button type="button" data-action="pair-approve" .button-link {
"yes, link this device"
}
}
}
}
}
pub(crate) fn pair_join(name: &str) -> Markup {
html! {
(site_header(&Host::Tenant(name.to_string())))
main.apex-main {
div.col-chat {
section.step.step-unclaimed {
h2.unclaimed-name { (name) ".localharness.xyz" }
p.step-msg {
"link this device to " (name) "? it'll be able to act \
as " (name) " without copying any keys."
}
button type="button" data-action="pair-join" .button-link {
"link this device"
}
div #pair-join-msg .step-msg {}
}
}
}
}
}
pub(crate) fn adopt_panel(code: &str, url: &str) -> Markup {
html! {
div #pair-slot .pair-slot.pair-active {
div.pair-instructions { "scan this on your other device" }
@if let Some(svg) = pair_qr_svg(url) {
div.pair-qr { (PreEscaped(svg)) }
}
div.pair-code-row {
span.pair-code-label { "code" }
code.pair-code { (code) }
}
div.pair-waiting { "type the code on that device to decrypt + import your seed" }
button type="button" data-action="pair-cancel" .ghost { "done" }
}
}
}
pub(crate) fn adopt_join(ct_hex: &str) -> Markup {
html! {
(site_header(&Host::Apex))
main.apex-main {
div.col-chat {
section.step {
div.pair-instructions { "adopt your identity on this device" }
form.create-form data-action="adopt-device" {
input #adopt-code .create-input type="text"
placeholder="enter code" autocomplete="off"
spellcheck="false" maxlength="8" required {}
input #adopt-ct type="hidden" value=(ct_hex) {}
button type="submit" .create-button { "adopt" }
}
div #adopt-msg .step-msg {}
}
}
}
}
}
pub(crate) fn identity_choice(name: &str) -> Markup {
html! {
div #agents-list .agents-list {
div.pair-instructions { "no identity on this device yet" }
div.pair-slot {
button type="button" data-action="create-new-claim" data-arg=(name) .ghost {
"create a new identity"
}
}
div.pair-slot {
button type="button" data-action="show-import" .ghost {
"i already have one โ import seed"
}
}
div #import-slot {}
div #seed-msg .admin-msg-slot {}
div.pair-waiting { "or open โadd a deviceโ on a device you already use" }
}
}
}
pub(crate) fn admin_security_collapsed() -> Markup {
html! {
div #security-slot .admin-section {
div.admin-section-title { "security" }
button type="button" data-action="reveal-security" .ghost {
"seed phrase, import, reset"
}
}
}
}
pub(crate) fn admin_security_expanded() -> Markup {
html! {
div #security-slot .admin-section {
div.admin-section-title { "security" }
div.admin-subsection {
div.admin-subsection-title { "seed phrase" }
div #seed-reveal .seed-reveal {
button type="button" data-action="reveal-seed" .ghost { "reveal" }
}
}
div.admin-subsection {
div.admin-subsection-title { "import a different seed" }
(import_seed_inline())
}
div.admin-subsection {
div.admin-subsection-title { "reset this device" }
div #reset-confirm-slot {
button type="button" data-action="reset-arm" .ghost { "resetโฆ" }
}
}
button type="button" data-action="hide-security" .ghost { "hide" }
}
}
}
pub(crate) fn reset_confirm_inline() -> Markup {
html! {
div #reset-confirm-slot .reset-confirm {
span.reset-confirm-prompt { "type RESET to clear this device โ identity + names are kept" }
input #reset-confirm-text .redeem-input type="text" placeholder="RESET";
div.reset-confirm-actions {
button type="button" data-action="reset-confirm" .danger { "reset" }
button type="button" data-action="reset-cancel" .ghost { "cancel" }
}
div #reset-confirm-msg .admin-msg-slot {}
}
}
}
pub(crate) fn reset_armed_inline() -> Markup {
html! {
div #reset-confirm-slot {
button type="button" data-action="reset-arm" .ghost { "resetโฆ" }
}
}
}
pub(crate) fn opfs_wipe_armed_inline() -> Markup {
html! {
span #opfs-wipe-slot {
button data-action="opfs-wipe" { "wipe" }
}
}
}
pub(crate) fn opfs_wipe_confirm_inline() -> Markup {
html! {
span #opfs-wipe-slot .opfs-wipe-confirm {
button data-action="opfs-wipe-confirm" .danger { "wipe?" }
button data-action="opfs-wipe-cancel" .ghost { "no" }
}
}
}
#[allow(dead_code)]
pub(crate) fn pricing_card(price_wei: u128) -> Markup {
html! {
section .pricing-card {
div.pricing-header {
div.pricing-title { "pricing" }
}
(pricing_card_body(price_wei, true))
}
}
}
#[allow(dead_code)] pub(crate) fn pricing_readonly_line(price_wei: u128) -> Markup {
let display = if price_wei == 0 {
"free".to_string()
} else {
format!("{} $LH/turn", super::format_wei_as_test_eth(price_wei))
};
html! {
div.financial-line {
span.financial-label { "pricing" }
span.financial-value { (display) }
}
}
}
pub(crate) fn financial_card(
name: &str,
tba_hex: &str,
owner_hex: &str,
lh_balance_wei: u128,
_price_wei: u128,
_is_owner: bool,
) -> Markup {
let tba_url = format!("https://moderato.tempo.xyz/address/{tba_hex}");
let owner_url = format!("https://moderato.tempo.xyz/address/{owner_hex}");
let balance_display = super::format_wei_as_test_eth(lh_balance_wei);
let rpc_url = format!("https://{name}.localharness.xyz/?rpc=1");
let tool_count = BuiltinTool::ALL.len();
html! {
section #financial-slot .financial-card {
div.financial-line {
span.financial-label { "name" }
span.financial-value { (name) }
}
div.financial-line {
span.financial-label { "owner" }
a.financial-tba href=(owner_url) target="_blank" rel="noopener"
title=(owner_hex) {
(short_addr(owner_hex))
}
}
div.financial-line {
span.financial-label { "wallet" }
a.financial-tba href=(tba_url) target="_blank" rel="noopener"
title=(tba_hex) {
(short_addr(tba_hex))
}
}
div.financial-line {
span.financial-label { "balance" }
span.financial-value.financial-balance { (balance_display) }
}
div.financial-line {
span.financial-label { "tools" }
span #tools-count .financial-value { (tool_count) }
}
div.financial-line {
span.financial-label { "rpc" }
a.financial-tba href=(rpc_url) target="_blank" rel="noopener"
title="inter-agent RPC endpoint" {
"?rpc=1"
}
}
}
}
}
pub(crate) fn pricing_card_body(price_wei: u128, is_owner: bool) -> Markup {
let display = if price_wei == 0 {
"free".to_string()
} else {
format!("{} $localharness/turn", super::format_wei_as_test_eth(price_wei))
};
html! {
div #pricing-body .pricing-body {
div.pricing-value { (display) }
@if is_owner {
div.pricing-edit {
input #pricing-input
type="text"
inputmode="decimal"
placeholder="1.0"
value=(if price_wei == 0 { String::new() } else { super::format_wei_as_test_eth(price_wei) }) {}
span.pricing-unit { "$localharness/turn" }
button.ghost
type="button"
data-action="pricing-save" { "save" }
}
div #pricing-msg .pricing-msg {}
}
}
}
}
pub(crate) fn import_seed_inline() -> Markup {
html! {
div #import-slot .seed-import {
textarea #import-seed
placeholder="paste 12 words separated by spaces"
rows="3" {}
div.seed-import-actions {
button type="button" data-action="import-seed" { "import" }
button type="button" data-action="cancel-import" .ghost { "cancel" }
}
div #seed-msg .step-msg {}
}
}
}
pub(crate) fn agents_list(
agents: &[crate::app::registry::OwnedToken],
main_token_id: u64,
) -> Markup {
if agents.is_empty() {
return html! {
div #agents-list .agents-list .agents-empty {}
};
}
html! {
div #agents-list .agents-list {
ul.agents-rows {
@for agent in agents {
li.agent-row {
a.agent-row-line
href=(format!("https://{}.localharness.xyz/", agent.name)) {
span.agent-name { (agent.name) }
span.agent-row-spacer {}
@if main_token_id != 0 && agent.token_id == main_token_id {
span.main-badge title="primary identity" { "main" }
} @else {
span.alt-badge title="secondary identity" { "alt" }
}
}
}
}
}
}
}
}
pub(crate) fn agent_act_panel(
token_id: u64,
tba_address: &str,
lh_balance_wei: u128,
) -> Markup {
let lh_whole = lh_balance_wei / 1_000_000_000_000_000_000u128;
html! {
div.agent-act-rows {
div.agent-act-row {
span.agent-act-label { "wallet" }
a.agent-act-value
href=(format!("https://moderato.tempo.xyz/address/{tba_address}"))
target="_blank" rel="noopener"
title=(tba_address) {
(short_addr(tba_address))
}
}
div.agent-act-row {
span.agent-act-label { "balance" }
code.agent-act-value { (lh_whole) " LH" }
}
}
form.agent-act-form
data-action="agent-send-lh"
data-arg=(token_id) {
input
id=(format!("agent-send-to-{token_id}"))
type="text"
placeholder="recipient 0xโฆ"
autocomplete="off"
spellcheck="false"
maxlength="42" {}
input
id=(format!("agent-send-amt-{token_id}"))
type="text"
placeholder="amount LH"
autocomplete="off"
spellcheck="false"
inputmode="decimal" {}
button type="submit" .ghost { "send" }
}
div
id=(format!("agent-act-msg-{token_id}"))
.admin-msg-slot {}
}
}
pub(crate) fn seed_phrase(words: &str) -> Markup {
html! {
div.seed-words { (words) }
p.apex-fine {
"12 words above. close this page or click "
button type="button" data-action="hide-seed" .link-button { "hide" }
" when you're done."
}
}
}
pub(crate) fn signer_no_identity() -> Markup {
html! {
main.apex-main {
div.col-chat {
section.apex-hero {
h2.apex-headline { "localharness signer" }
p.apex-sub {
"no identity exists on this device yet, so this signer "
"tab can't sign anything. "
a href="https://localharness.xyz/" { "go to apex" }
" to create or import one."
}
}
}
}
}
}
pub(crate) fn signer_chrome(address_hex: &str) -> Markup {
html! {
main.apex-main {
div.col-chat {
section.apex-hero {
h2.apex-headline { "localharness signer" }
p.apex-sub {
"this tab is acting as a signing service for an embedded "
"subdomain. it will sign authentication challenges from "
"any *.localharness.xyz origin using the master wallet:"
}
div.wallet-address-row {
span.wallet-label { "address" }
code .wallet-address { (address_hex) }
}
p.apex-fine {
"if you opened this manually rather than via an iframe, "
a href="https://localharness.xyz/" { "go home" }
"."
}
}
}
}
}
}
pub(crate) fn unclaimed(host: &Host, name: &str) -> Markup {
html! {
(site_header(host))
main.apex-main {
div.col-chat {
section.step.step-unclaimed {
h2.unclaimed-name { (name) ".localharness.xyz" }
p.step-msg {
"this name is open. claim it to make it the home of an agent you own."
}
button type="button" data-action="claim-on-chain" .button-link {
"claim " (name)
}
div #claim-msg .step-msg {}
}
}
}
}
}
pub(crate) fn opfs_breadcrumb(cwd: &[String]) -> Markup {
html! {
a data-action="opfs-nav" data-arg="" { "/" }
@for i in 0..cwd.len() {
@let arg = cwd[..=i].join("/");
a data-action="opfs-nav" data-arg=(arg) { (cwd[i]) "/" }
}
}
}
pub(crate) fn opfs_list(cwd: &[String], entries: &[DirEntry]) -> Markup {
html! {
@if entries.is_empty() {
li.empty { "(empty)" }
} @else {
@for entry in entries {
@match entry.kind {
EntryKind::Directory => {
@let arg = if cwd.is_empty() {
entry.name.clone()
} else {
format!("{}/{}", cwd.join("/"), entry.name)
};
li.dir data-action="opfs-nav" data-arg=(arg) {
span.name { (entry.name) }
}
}
_ => {
@let lname = entry.name.to_ascii_lowercase();
@let opens_display = lname.ends_with(".html")
|| lname.ends_with(".htm")
|| lname.ends_with(".rl");
li.file {
span.name data-action="opfs-open" data-arg=(entry.name) {
(entry.name)
}
@if let Some(size) = entry.size {
span.size { (format_bytes(size)) }
}
@if opens_display {
button.file-edit
type="button"
data-action="opfs-edit"
data-arg=(entry.name)
title=(format!("edit {}", entry.name)) { "edit" }
}
button.file-delete
type="button"
data-action="opfs-delete"
data-arg=(entry.name)
aria-label=(format!("delete {}", entry.name))
title=(format!("delete {}", entry.name)) { "ร" }
}
}
}
}
}
}
}
pub(crate) fn opfs_error(message: &str) -> Markup {
html! {
li.empty { "error: " (message) }
}
}
pub(crate) fn opfs_editor(display_path: &str, name: &str, text: &str) -> Markup {
html! {
div.editor {
div.editor-header {
span.editor-path { (display_path) }
div.editor-actions {
button.panel-button
type="button"
data-action="opfs-save"
data-arg=(name) { "save" }
button.panel-button
type="button"
data-action="opfs-close-viewer" { "close" }
}
}
textarea #fs-editor .editor-textarea { (text) }
}
}
}
pub(crate) fn display_surface() -> Markup {
html! {
div.display-wrap {
div.display-stage {
canvas #display-canvas .display-canvas {}
}
}
}
}
pub(crate) fn public_landing(
name: &str,
owner: Option<&str>,
tba: Option<&str>,
main_name: Option<&str>,
is_main: bool,
siblings: &[crate::app::registry::OwnedToken],
personas: &[Option<String>],
owner_overlay: bool,
) -> Markup {
html! {
div.public-face {
@if owner_overlay {
a.app-edit href="?edit=1" title="back to your studio" { "studio" }
}
header.public-hero {
h1.public-title { (name) }
p.public-tagline {
"agent on localharness"
@if is_main { " ยท " span.main-badge title="primary identity" { "main" } }
}
}
div.public-meta {
@if let Some(addr) = owner {
div.public-meta-row {
span.public-meta-label { "owner" }
@if let Some(m) = main_name {
a.public-meta-value
href=(format!("https://{m}.localharness.xyz/"))
title=(addr) { (m) }
} @else {
a.public-meta-value
href=(format!("https://moderato.tempo.xyz/address/{addr}"))
target="_blank" rel="noopener" title=(addr) { (short_addr(addr)) }
}
}
}
@if let Some(t) = tba {
div.public-meta-row {
span.public-meta-label { "wallet" }
a.public-meta-value
href=(format!("https://moderato.tempo.xyz/address/{t}"))
target="_blank" rel="noopener" title=(t) { (short_addr(t)) }
}
}
}
@if !siblings.is_empty() {
section.public-directory {
h2.public-section-title { "more agents by this owner" }
ul.agents-rows {
@for (i, s) in siblings.iter().enumerate() {
@let preview = personas.get(i).and_then(|p| p.as_deref());
li.agent-row {
a.agent-card
href=(format!("https://{}.localharness.xyz/", s.name)) {
span.agent-name { (s.name) }
@if let Some(p) = preview {
span.agent-preview { (truncate_preview(p, 80)) }
}
}
}
}
}
}
}
footer.public-footer {
a href="https://localharness.xyz/" title="localharness" { "localharness" }
}
}
}
}
pub(crate) fn app_fullscreen(owner_overlay: bool) -> Markup {
html! {
div.app-fullscreen {
div.app-stage {
canvas #display-canvas .display-canvas {}
}
@if owner_overlay {
a.app-edit href="?edit=1" title="back to your studio" { "studio" }
}
}
}
}
fn format_bytes(n: u64) -> String {
if n < 1024 {
format!("{n} B")
} else if n < 1024 * 1024 {
format!("{:.1} KB", n as f64 / 1024.0)
} else {
format!("{:.1} MB", n as f64 / (1024.0 * 1024.0))
}
}
pub(crate) fn keymeta(key: &str) -> Markup {
let n = key.len();
if n == 0 {
return html! {};
}
let looks_right = (30..=60).contains(&n)
&& key
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
let suffix = if looks_right { "" } else { " - check" };
html! {
span style=(if looks_right { "" } else { "color: var(--error)" }) {
"(" (n) " chars" (suffix) ")"
}
}
}