diff --git a/crates/deckard-app/src/activity_view.rs b/crates/deckard-app/src/activity_view.rs index a41e1a6..edfc386 100644 --- a/crates/deckard-app/src/activity_view.rs +++ b/crates/deckard-app/src/activity_view.rs @@ -23,7 +23,7 @@ //! had no human and renders neutral. An executed shield reads the *result* ("moved … ETH to your //! private balance"), the demo's payoff. //! -//! Each row carries the two-actor chain (the cyan agent squircle for Atlas, neutral for a +//! Each row carries the two-actor chain (the cyan `agent_mark` for the agent, neutral for a //! foreground app action), the lifecycle glyph, the real broadcast `tx_hash`, a relative //! timestamp, and — for an over-cap/over-scope proposal — the ACTUAL breached fence (per-tx vs //! daily, never a hardcoded cite). A header STOP control is the always-reachable panic brake. @@ -54,14 +54,14 @@ use deckard_contract::{ use crate::money::money; use crate::shell::Shell; -use crate::shell_chrome::agent_squircle; use crate::theme; +use crate::widgets::agent_mark; -/// The displayed subject for an action's origin: the agent's name when an agent acted, "You" -/// when the foreground app did. One agent in the demo scope (Atlas). -fn origin_subject(origin: ProposalOrigin) -> &'static str { +/// The displayed subject for an action's origin: the agent's handle when an agent acted, "You" +/// when the foreground app did (E2, #182 — one agent in demo scope, named via `Shell::agent_handle`). +fn origin_subject(origin: ProposalOrigin, agent_handle: &str) -> &str { match origin { - ProposalOrigin::Agent => "Atlas", + ProposalOrigin::Agent => agent_handle, ProposalOrigin::App => "You", } } @@ -434,12 +434,10 @@ impl Shell { .text_color(fg) .child("Activity"), ) - .child( - div() - .text_sm() - .text_color(muted) - .child("What Atlas and you did this session. Watch it, and stop it."), - ), + .child(div().text_sm().text_color(muted).child(format!( + "What {} and you did this session. Watch it, and stop it.", + self.agent_handle() + ))), ) .child(self.activity_stop_control(cx)) } @@ -549,11 +547,11 @@ impl Shell { /// One dense feed row — the two-actor chain + the outcome cluster. /// - /// - **Agent, auto-allowed within cap** (no human in the loop): `[A] Atlas · shield 0.05 ETH + /// - **Agent, auto-allowed within cap** (no human in the loop): `[K] Kyoto · shield 0.05 ETH /// · auto-approved within cap ✓ 0x6ea…9f3c · 4m ago` (muted label, green glyph). - /// - **Agent, executed shield** (the demo payoff): `[A] Atlas · shield 0.05 ETH · moved + /// - **Agent, executed shield** (the demo payoff): `[K] Kyoto · shield 0.05 ETH · moved /// 0.05 ETH to your private balance ✓ 0x6ea…9f3c · 4m ago`. - /// - **Agent, proposed (over cap / mainnet)** — lives in NEEDS YOU: `[A] Atlas · … → You + /// - **Agent, proposed (over cap / mainnet)** — lives in NEEDS YOU: `[K] Kyoto · … → You /// waiting … over per-tx cap`, plus the `⌘⏎ · x` hint on the SELECTED row and a hover-only /// "Review →" for the mouse. /// - **Agent, human-approved/denied** (`!auto_allowed`, settled): the outcome label tints @@ -583,6 +581,7 @@ impl Shell { let amber = theme::amber(is_dark); let is_agent = record.origin == ProposalOrigin::Agent; + let agent_handle = self.agent_handle(); let proposed = is_proposed(record); let selected = proposed && selected_id == Some(record.request_id); // A human is in the chain for anything that was NOT auto-allowed hands-free — an over-cap @@ -594,13 +593,19 @@ impl Shell { let needed_human = human_acted(record); let summary = payload_summary(&record.payload, self.mask); - // The lead glyph: the cyan agent squircle for an agent, a neutral identity square for an - // app action — both static. + // The lead glyph: the cyan agent mark (handle-seeded) for an agent, a neutral identity + // square for an app action — both static. let lead = if is_agent { - agent_squircle(px(20.0), px(6.0), agent, agent_tint) + agent_mark( + &agent_handle, + crate::tokens::MARK_MD, + crate::tokens::RADIUS_ROW, + agent, + agent_tint, + ) } else { div() - .size(px(20.0)) + .size(crate::tokens::MARK_MD) .rounded(crate::tokens::RADIUS_ROW) .bg(theme::identity_square(is_dark)) .into_any_element() @@ -618,7 +623,7 @@ impl Shell { .text_sm() .font_weight(FontWeight::MEDIUM) .text_color(fg) - .child(origin_subject(record.origin).to_string()), + .child(origin_subject(record.origin, &agent_handle).to_string()), ) .child(div().flex_shrink_0().text_sm().text_color(muted).child("·")) // The verb + object is the part that grows and clamps: it gets the flex space and @@ -843,16 +848,23 @@ impl Shell { let agent_tint = theme::agent_tint(is_dark); let is_agent = record.origin == ProposalOrigin::Agent; - let subject = origin_subject(record.origin); + let agent_handle = self.agent_handle(); + let subject = origin_subject(record.origin, &agent_handle); let cite = cite_phrase(record.reason).unwrap_or("held for your approval"); let band = { let lead = if is_agent { - agent_squircle(px(24.0), px(7.0), agent, agent_tint) + agent_mark( + &agent_handle, + crate::tokens::MARK_MD, + crate::tokens::RADIUS_ROW, + agent, + agent_tint, + ) } else { div() - .size(px(24.0)) - .rounded(px(7.0)) + .size(crate::tokens::MARK_MD) + .rounded(crate::tokens::RADIUS_ROW) .bg(theme::identity_square(is_dark)) .into_any_element() }; diff --git a/crates/deckard-app/src/agent_view.rs b/crates/deckard-app/src/agent_view.rs index af7dfb8..872fc8f 100644 --- a/crates/deckard-app/src/agent_view.rs +++ b/crates/deckard-app/src/agent_view.rs @@ -18,11 +18,11 @@ use gpui_component::{ use crate::shell::Shell; use crate::theme; use crate::welcome::{agent_policy_rows, fraction}; -use crate::widgets::{budget_gauge, section_label}; +use crate::widgets::{agent_mark, budget_gauge, section_label}; impl Shell { - /// The agent surface for the selected agent (currently the one agent, Atlas): - /// identity + a plain-language autonomy statement + the limits/scope + a budget + /// The agent surface for the selected agent (currently the one agent, handle `Kyoto` by + /// default): identity + a plain-language autonomy statement + the limits/scope + a budget /// gauge + controls (Pause / Rotate / Adjust / Revoke and STOP) + what this /// agent did. Built from `self.agent_policy` (the live daemon fence). pub fn render_agent_surface(&self, cx: &mut Context) -> impl IntoElement { @@ -38,6 +38,7 @@ impl Shell { let agent = theme::agent(is_dark); let agent_tint = theme::agent_tint(is_dark); let amber = theme::amber(is_dark); + let agent_handle = self.agent_handle(); // Derive the per-tx cap (ETH) for the plain-language autonomy line — never // invented; the same number the fence shows. `None` until the first fetch, or @@ -68,9 +69,10 @@ impl Shell { .w_full() .items_center() .gap_3() - .child(crate::shell_chrome::agent_squircle( - px(34.0), - px(9.0), + .child(agent_mark( + &agent_handle, + crate::tokens::MARK_LG, + crate::tokens::RADIUS_ROW, agent, agent_tint, )) @@ -79,7 +81,7 @@ impl Shell { .text_xl() .font_weight(FontWeight::SEMIBOLD) .text_color(fg) - .child("Atlas"), + .child(agent_handle.clone()), ) // A small "acting" status — cyan, the agent actor signal. .child( @@ -132,8 +134,8 @@ impl Shell { let spent_eth = deckard_core::format_amount(p.spent_today_wei, 18, 6); let mono_rows = mono.clone(); let autonomy = format!( - "Atlas acts on its own under {cap} ETH per move and asks you above \ - that. It can shield ETH only. It never holds your key, and it \ + "{agent_handle} acts on its own under {cap} ETH per move and asks you \ + above that. It can shield ETH only. It never holds your key, and it \ cannot send to a new address." ); diff --git a/crates/deckard-app/src/main.rs b/crates/deckard-app/src/main.rs index e3e7ac2..dfaaa18 100644 --- a/crates/deckard-app/src/main.rs +++ b/crates/deckard-app/src/main.rs @@ -12,6 +12,7 @@ mod commit_flow; mod commit_view; mod errors; mod money; +mod names; mod onboarding; mod palette; mod palette_commands; diff --git a/crates/deckard-app/src/names.rs b/crates/deckard-app/src/names.rs new file mode 100644 index 0000000..f0a9d1a --- /dev/null +++ b/crates/deckard-app/src/names.rs @@ -0,0 +1,150 @@ +//! Identity naming — the human-readable name a wallet or the agent shows before (and unless) the +//! operator renames it (E2, #182). DESIGN.md §"Identity is named": wallets/agents carry a real +//! name/handle; the literal word Wallet is never an entity label, and the breadcrumb names the +//! entity, never a project prefix. +//! +//! A fresh vault reads like `Meridian`, not `Wallet` or a raw `0x…` address. The wallet name is +//! derived deterministically from the account address (stable across launches for the same key, so +//! it never flickers), and the agent's handle is assigned from a rotating city list (retiring the +//! old fixed placeholder). Both are overridable: the override is a persisted `Settings` field read +//! by `Shell::wallet_name` / `Shell::agent_handle`; this module only supplies the default when no +//! override is set. + +/// Curated wallet codenames — calm place/landmark words, never crypto jargon (DESIGN.md §Language). +/// The default name is one of these, chosen deterministically from the account address so the same +/// key always reads the same, yet two different wallets read differently. `Meridian` leads so the +/// golden-ref demo account reads like the reference. +const WALLET_NAMES: &[&str] = &[ + "Meridian", "Harbor", "Vantage", "Beacon", "Cascade", "Summit", "Anchor", "Haven", "Compass", + "Vista", "Keystone", "Bastion", +]; + +/// Auto-assigned agent handles — a rotating city list (DESIGN.md §request-origin model: "non-human +/// sessions get an auto-assigned handle"). Index 0 is the first agent's default (`Kyoto`), which +/// retires the old fixed placeholder handle. +const AGENT_HANDLES: &[&str] = &[ + "Kyoto", "Osaka", "Lisbon", "Oslo", "Nairobi", "Quito", "Bergen", "Cairo", +]; + +/// A small, dependency-free FNV-1a hash of `seed` — deterministic (never `rand`), so the same +/// address always maps to the same codename across launches. +fn fnv1a(seed: &str) -> u64 { + const OFFSET: u64 = 0xcbf2_9ce4_8422_2325; + const PRIME: u64 = 0x0000_0100_0000_01b3; + let mut hash = OFFSET; + for b in seed.as_bytes() { + hash ^= u64::from(*b); + hash = hash.wrapping_mul(PRIME); + } + hash +} + +/// The deterministic default name for the wallet at `address` (its EIP-55 string in practice). An +/// empty seed falls to the first name, so a not-yet-unlocked shell still reads sensibly. +pub(crate) fn default_wallet_name(address: &str) -> &'static str { + let idx = (fnv1a(address) as usize) % WALLET_NAMES.len(); + WALLET_NAMES[idx] +} + +/// The default handle for the agent at `index` (one agent in v1 scope → index 0 = `Kyoto`); wraps +/// if there are ever more agents than curated handles. +pub(crate) fn default_agent_handle(index: usize) -> &'static str { + AGENT_HANDLES[index % AGENT_HANDLES.len()] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wallet_name_is_deterministic_and_curated() { + let a = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + // Same address → same name, every call (no rng, no per-launch drift). + assert_eq!(default_wallet_name(a), default_wallet_name(a)); + assert!(WALLET_NAMES.contains(&default_wallet_name(a))); + // Never the forbidden literal, never a raw address. + assert_ne!(default_wallet_name(a), "Wallet"); + assert!(!default_wallet_name(a).starts_with("0x")); + // The empty (locked) seed still yields a real codename, not a blank. + assert!(!default_wallet_name("").is_empty()); + } + + #[test] + fn agent_handle_rotates_and_retires_placeholder() { + assert_eq!(default_agent_handle(0), "Kyoto"); + // Distinct handles for distinct indices; wraps past the list end. + assert_ne!(default_agent_handle(0), default_agent_handle(1)); + assert_eq!( + default_agent_handle(0), + default_agent_handle(AGENT_HANDLES.len()) + ); + } + + /// Reflective source-scan (mirrors `tokens::lint::no_raw_text_size_px_in_views`): the retired + /// *entity* labels — the bare word Wallet standing in for the wallet's name, and the old fixed + /// agent placeholder — must appear NOWHERE as a rendered string literal (E2 acceptance, #182). + /// Identity is named: the breadcrumb/masthead/sidebar name the entity (`Meridian` / `Kyoto`), + /// never a mode word. + /// + /// The needles are the EXACT quoted tokens (`"Wallet"` / `"Atlas"`), not a substring/word match, + /// and that precision is deliberate: the DoD bans the word only as an *entity* label ("the + /// breadcrumb names the entity ... never 'Wallet'"), while DESIGN + the golden ref use the word + /// descriptively everywhere — the `Wallets` sidebar group, the `This wallet` rail header, the + /// Settings `Wallet name` field. A substring match would false-positive on all of those; the + /// standalone `"Wallet"` literal is unambiguously the retired entity label. Comments/docs are + /// exempt (they explain the rule); this module is exempt (it names the needles). + #[test] + fn retired_identity_labels_are_never_rendered() { + use std::fs; + use std::path::Path; + + // Build the forbidden needles from chars so THIS file never contains the sequence it bans + // (`"{q}Wallet{q}"` is not `"Wallet"`), and pair each with a fix hint. + let q = '"'; + let forbidden = [ + ( + format!("{q}Wallet{q}"), + "name the entity (e.g. Meridian) via Shell::wallet_name", + ), + ( + format!("{q}Atlas{q}"), + "use the generated handle via Shell::agent_handle (e.g. Kyoto)", + ), + ]; + + let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut offenders = Vec::new(); + for entry in fs::read_dir(&src).expect("read src dir") { + let path = entry.expect("dir entry").path(); + if path.extension().and_then(|e| e.to_str()) != Some("rs") { + continue; + } + let name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + // This module names the forbidden needles (in the check + messages); skip it. + if name == "names.rs" { + continue; + } + let text = fs::read_to_string(&path).expect("read source"); + for (i, raw) in text.lines().enumerate() { + let line = raw.trim_start(); + if line.starts_with("//") { + continue; + } + for (needle, hint) in &forbidden { + if line.contains(needle.as_str()) { + offenders.push(format!("{name}:{} — {hint}", i + 1)); + } + } + } + } + assert!( + offenders.is_empty(), + "a retired identity label is rendered as a literal (DESIGN.md \"Identity is named\"):\n {}", + offenders.join("\n ") + ); + } +} diff --git a/crates/deckard-app/src/palette.rs b/crates/deckard-app/src/palette.rs index a48e6eb..060ed97 100644 --- a/crates/deckard-app/src/palette.rs +++ b/crates/deckard-app/src/palette.rs @@ -23,7 +23,6 @@ use gpui_component::{ use crate::palette_commands::{self, COMMANDS}; use crate::settings::ThemeModePref; use crate::shell::{Selection, Shell}; -use crate::shell_chrome::short_addr; use crate::theme; /// The live display label for a command — the registry title, except mask/theme flip to @@ -105,8 +104,10 @@ impl Shell { }), ); - // --- Context line: the active scope (wallet identity / project). --- - let context_line = self.palette_context_line(id_square, muted, mono.clone()); + // --- Context line: the active scope (wallet identity / agent / project). --- + let agent = theme::agent(is_dark); + let agent_tint = theme::agent_tint(is_dark); + let context_line = self.palette_context_line(id_square, fg, agent, agent_tint, muted); // --- Results: the ranked rows (or an empty-state line). --- let results = if self.palette_results.is_empty() { @@ -188,48 +189,40 @@ impl Shell { ) } - /// The context line under the query: the active scope. A wallet selection shows the - /// identity square + truncated mono address; the project shows the identity square + - /// "Personal". + /// The context line under the query: the active scope, named (E2, #182). A wallet shows its + /// identity mark + name (`Meridian`); the agent shows its cyan mark + handle (`Kyoto`); the + /// project shows its mark + name — never the literal word Wallet, never a raw address. fn palette_context_line( &self, id_square: gpui::Hsla, + fg: gpui::Hsla, + agent: gpui::Hsla, + agent_tint: gpui::Hsla, muted: gpui::Hsla, - mono: SharedString, ) -> impl IntoElement { let row = h_flex().items_center().gap_2().px_4().pb_2().text_xs(); + let sm = crate::tokens::MARK_SM; + let radius = crate::tokens::RADIUS_ROW; match self.selection { Selection::Wallet => { - let addr = short_addr(&self.wallet_address_string()); - let label = if addr.is_empty() { - "Wallet".to_string() - } else { - addr - }; - row.child( - div() - .size(px(14.0)) - .rounded(crate::tokens::RADIUS_INPUT) - .bg(id_square), - ) - .child(div().font_family(mono).text_color(muted).child(label)) + let name = self.wallet_name(); + row.child(crate::widgets::identity_mark( + &name, sm, radius, id_square, fg, + )) + .child(div().text_color(muted).child(name)) } Selection::Project => row - .child( - div() - .size(px(14.0)) - .rounded(crate::tokens::RADIUS_INPUT) - .bg(id_square), - ) + .child(crate::widgets::identity_mark( + "Personal", sm, radius, id_square, fg, + )) .child(div().text_color(muted).child("Personal")), - Selection::Agent => row - .child( - div() - .size(px(14.0)) - .rounded(crate::tokens::RADIUS_INPUT) - .bg(id_square), - ) - .child(div().text_color(muted).child("Atlas")), + Selection::Agent => { + let handle = self.agent_handle(); + row.child(crate::widgets::agent_mark( + &handle, sm, radius, agent, agent_tint, + )) + .child(div().text_color(muted).child(handle)) + } } } diff --git a/crates/deckard-app/src/palette_commands.rs b/crates/deckard-app/src/palette_commands.rs index 0c41ae8..d15911b 100644 --- a/crates/deckard-app/src/palette_commands.rs +++ b/crates/deckard-app/src/palette_commands.rs @@ -80,6 +80,22 @@ pub const COMMANDS: &[Command] = &[ shortcut: Some("⌘,"), icon: Some(IconName::Settings), }, + // Rename the wallet / agent (E2, #182). Both open Settings and focus their field. No bundled + // pencil glyph, so no icon. + Command { + id: "rename-wallet", + title: "Rename wallet", + aliases: &["rename", "name", "wallet name", "label", "rename wallet"], + shortcut: None, + icon: None, + }, + Command { + id: "rename-agent", + title: "Rename agent", + aliases: &["rename agent", "handle", "agent name", "agent handle"], + shortcut: None, + icon: None, + }, Command { id: "copy", title: "Copy address", diff --git a/crates/deckard-app/src/settings.rs b/crates/deckard-app/src/settings.rs index caa2f7d..6e92468 100644 --- a/crates/deckard-app/src/settings.rs +++ b/crates/deckard-app/src/settings.rs @@ -41,7 +41,16 @@ impl ThemeModePref { #[serde(default)] pub struct Settings { pub theme_mode: ThemeModePref, - pub display_name: String, + /// The wallet's human name, shown in the masthead/breadcrumb/sidebar (E2, #182). Empty = the + /// deterministic default from the account address ([`crate::names::default_wallet_name`]); a + /// non-empty value is the operator's rename. The `display_name` alias migrates the value from + /// the pre-#182 "Display name" field (its description was already "a label for this wallet + /// profile") so an upgrade never silently drops a name the user had set. + #[serde(alias = "display_name")] + pub wallet_name: String, + /// The agent's handle, shown wherever the agent is named (E2, #182). Empty = the auto-assigned + /// default ([`crate::names::default_agent_handle`]); a non-empty value is the operator's rename. + pub agent_handle: String, pub launch_minimized: bool, /// Custom Ethereum RPC URL (bring-your-own-RPC). Empty = the bundled default. /// The trustless default (a local Helios light client) supersedes this later. @@ -68,7 +77,8 @@ impl Default for Settings { fn default() -> Self { Self { theme_mode: ThemeModePref::Dark, - display_name: String::new(), + wallet_name: String::new(), + agent_handle: String::new(), launch_minimized: false, rpc_url: String::new(), watch_address: String::new(), @@ -220,6 +230,24 @@ impl Settings { mod tests { use super::*; + #[test] + fn legacy_display_name_migrates_into_wallet_name() { + // A pre-#182 config carried the value in `display_name` (the old "Display name" field, + // already described as a wallet-profile label). The serde alias must carry it into + // `wallet_name` so an upgrade never silently drops a name the user had set. + let legacy: Settings = + serde_json::from_str(r#"{"display_name":"Treasury"}"#).expect("legacy config parses"); + assert_eq!(legacy.wallet_name, "Treasury"); + // A new config uses the canonical field name. + let current: Settings = + serde_json::from_str(r#"{"wallet_name":"Treasury"}"#).expect("current config parses"); + assert_eq!(current.wallet_name, "Treasury"); + // Round-trip writes the canonical name (the alias is read-only). + let json = serde_json::to_string(¤t).expect("serialize"); + assert!(json.contains("wallet_name")); + assert!(!json.contains("display_name")); + } + #[test] fn rpc_resolution_prefers_env_then_setting_then_per_chain_default() { let mainnet_default = deckard_core::DEFAULT_RPC; diff --git a/crates/deckard-app/src/settings_view.rs b/crates/deckard-app/src/settings_view.rs index 3bb2c4d..63314ad 100644 --- a/crates/deckard-app/src/settings_view.rs +++ b/crates/deckard-app/src/settings_view.rs @@ -81,7 +81,12 @@ impl Shell { .child(mode_button("mode-light", "Light", ThemeModePref::Light)) .into_any_element(); - let name_control = Input::new(&self.name_input).w(px(220.0)).into_any_element(); + let wallet_name_control = Input::new(&self.wallet_name_input) + .w(px(220.0)) + .into_any_element(); + let agent_handle_control = Input::new(&self.agent_handle_input) + .w(px(220.0)) + .into_any_element(); let rpc_control = Input::new(&self.rpc_input).w(px(260.0)).into_any_element(); let watch_control = Input::new(&self.watch_input) .w(px(260.0)) @@ -150,21 +155,29 @@ impl Shell { watch_control, )), ) - .child(section_label("Profile", muted)) + .child(section_label("Names", muted)) .child( card() .child(row( - "Display name", - "A label for this wallet profile.", - name_control, + "Wallet name", + "The name shown in the breadcrumb and masthead. Blank uses an auto-assigned name.", + wallet_name_control, )) .child(divider(border)) .child(row( - "Start in menu bar", - "Launch minimized.", - launch_control, + "Agent handle", + "The handle shown wherever the agent is named. Blank uses an auto-assigned handle.", + agent_handle_control, )), ) + .child(section_label("General", muted)) + .child( + card().child(row( + "Start in menu bar", + "Launch minimized.", + launch_control, + )), + ) .child(div().pt_2().text_xs().text_color(muted).child(format!( "Preferences are stored at {}", Settings::config_path_display() diff --git a/crates/deckard-app/src/shell.rs b/crates/deckard-app/src/shell.rs index f92618f..9153a3b 100644 --- a/crates/deckard-app/src/shell.rs +++ b/crates/deckard-app/src/shell.rs @@ -68,10 +68,11 @@ fn write_then_unlock( /// What the sidebar tree selects — the contextual-view driver. The home surface /// renders differently per selection (project / wallet). Demo scope is a single -/// project + wallet; and Atlas, the agent. Atlas is now a FIRST-CLASS entity -/// (DESIGN.md v2 §The agent interaction model): a standalone sidebar row that opens -/// its own surface (policy + controls + its activity), no longer folded into the -/// wallet home. It is still key-less automation on the same wallet EOA. +/// project + wallet, and the agent (auto-named `Kyoto` by default — see +/// [`crate::names`]). The agent is a FIRST-CLASS entity (DESIGN.md v2 §The agent +/// interaction model): a standalone sidebar row that opens its own surface (policy + +/// controls + its activity), no longer folded into the wallet home. It is still +/// key-less automation on the same wallet EOA. #[derive(Clone, Copy, PartialEq, Eq)] pub enum Selection { Project, @@ -153,7 +154,12 @@ pub struct Shell { /// The active full-pane surface (Home = the selection's contextual view). pub surface: Surface, pub settings: Settings, - pub name_input: Entity, + /// The Settings "Wallet name" field — renames the wallet (E2, #182); empty reverts to the + /// deterministic default. Persisted to `settings.wallet_name` on change/blur. + pub wallet_name_input: Entity, + /// The Settings "Agent handle" field — renames the agent (E2, #182); empty reverts to the + /// auto-assigned default. Persisted to `settings.agent_handle` on change/blur. + pub agent_handle_input: Entity, pub rpc_input: Entity, pub watch_input: Entity, pub created: usize, @@ -183,7 +189,7 @@ pub struct Shell { /// figure (DESIGN §Trust). Initialised from `Settings.mask_balances` and persisted on /// every toggle — the inverse of the seed reveal's momentary, default-hidden model. pub mask: bool, - /// The signer's live policy fence, rendered on the wallet home's "What Atlas may do" + /// The signer's live policy fence, rendered on the wallet home's "what the agent may do" /// card so it shows the SAME numbers `deckard_policy_get` returns. Fetched from the /// daemon (`PolicyGet` deliberately succeeds while locked — the fence is config, not a /// secret); `None` until the first fetch lands or when the daemon is unreachable. @@ -382,21 +388,40 @@ impl Shell { let focus_handle = cx.focus_handle(); window.focus(&focus_handle, cx); - let name_input = cx.new(|cx| { + // Wallet name / agent handle (E2, #182): persist the rename as the user types (and on + // blur). Empty = revert to the deterministic default (see `Shell::wallet_name` / + // `agent_handle`), so clearing the field is a valid "reset to auto" gesture. + let wallet_name_input = cx.new(|cx| { InputState::new(window, cx) - .placeholder("Your name") - .default_value(settings.display_name.clone()) + .placeholder("Auto (e.g. Meridian)") + .default_value(settings.wallet_name.clone()) }); - - // Persist the text field as the user types (and on blur). - cx.subscribe(&name_input, |this, state, event: &InputEvent, cx| { + cx.subscribe(&wallet_name_input, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change | InputEvent::Blur) { - this.settings.display_name = state.read(cx).value().to_string(); + this.settings.wallet_name = state.read(cx).value().to_string(); this.settings.save(); + cx.notify(); } }) .detach(); + let agent_handle_input = cx.new(|cx| { + InputState::new(window, cx) + .placeholder("Auto (e.g. Kyoto)") + .default_value(settings.agent_handle.clone()) + }); + cx.subscribe( + &agent_handle_input, + |this, state, event: &InputEvent, cx| { + if matches!(event, InputEvent::Change | InputEvent::Blur) { + this.settings.agent_handle = state.read(cx).value().to_string(); + this.settings.save(); + cx.notify(); + } + }, + ) + .detach(); + // Custom RPC URL: persist as typed; apply (re-spawn the provider) on blur. let rpc_input = cx.new(|cx| { InputState::new(window, cx) @@ -642,7 +667,8 @@ impl Shell { selection: Selection::Wallet, surface: Surface::Home, settings, - name_input, + wallet_name_input, + agent_handle_input, rpc_input, watch_input, created: 0, @@ -1256,7 +1282,7 @@ impl Shell { self.start_balance_poll(cx); } - /// Fetch the daemon's live policy for the wallet home's "What Atlas may do" fence (off + /// Fetch the daemon's live policy for the wallet home's "what the agent may do" fence (off /// the UI thread). Key-less: `PolicyGet` is a read of the fence the daemon enforces — the /// daemon answers it even while locked, so this works from the unlock gate too. On any /// failure the card keeps its previous snapshot (or honestly shows none); never fabricates. @@ -1373,6 +1399,30 @@ impl Shell { .unwrap_or_default() } + /// The wallet's display name (E2, #182): the operator's rename (`settings.wallet_name`) when + /// set, else a deterministic codename from the account address — never the literal word Wallet. + /// A watched read-only address is named "Watched account" (it isn't your renamable wallet). + pub fn wallet_name(&self) -> String { + if self.viewing_watch { + return "Watched account".to_string(); + } + let renamed = self.settings.wallet_name.trim(); + if !renamed.is_empty() { + return renamed.to_string(); + } + crate::names::default_wallet_name(&self.wallet_address_string()).to_string() + } + + /// The agent's handle (E2, #182): the operator's rename (`settings.agent_handle`) when set, else + /// the auto-assigned default (index 0 → `Kyoto`) — retires the old fixed placeholder handle. + pub fn agent_handle(&self) -> String { + let renamed = self.settings.agent_handle.trim(); + if !renamed.is_empty() { + return renamed.to_string(); + } + crate::names::default_agent_handle(0).to_string() + } + /// The chain the daemon signs for (resolved once at startup). The swap surface reads it to /// pick the curated token list, the orderbook base, and the per-chain swatch. pub fn chain_id(&self) -> u64 { @@ -1643,7 +1693,7 @@ impl Shell { pub fn select(&mut self, sel: Selection, cx: &mut Context) { self.selection = sel; self.surface = Surface::Home; - // The wallet home now carries the "What Atlas may do" policy fence — re-fetch the + // The wallet home now carries the "what the agent may do" policy fence — re-fetch the // daemon's live policy on every visit so an out-of-band edit to policy.json (or a // STOP) shows up without a relaunch. if matches!(sel, Selection::Wallet | Selection::Agent) { @@ -2287,7 +2337,7 @@ impl Shell { cx.notify(); let client = self.signer.client(); let order_for_task = order.clone(); - // App-origin: the user's foreground GUI swap → the feed labels the order "You", not "Atlas". + // App-origin: the user's foreground GUI swap → the feed labels the order "You", not the agent handle. let task = cx.background_spawn(async move { client.propose_order_blocking(&order_for_task, deckard_contract::ProposalOrigin::App) }); @@ -2988,6 +3038,21 @@ impl Shell { self.stop_revoke_all(cx); } "settings" => self.open(Surface::Settings, cx), + // Rename the wallet / agent (E2, #182): open Settings and focus the field so the ⌘K + // command lands the operator directly on the input it named. Drop the captured + // prev-focus so `close_palette` (below) doesn't yank focus back off the field. + "rename-wallet" => { + self.open(Surface::Settings, cx); + self.palette_prev_focus = None; + self.wallet_name_input + .update(cx, |input, cx| input.focus(window, cx)); + } + "rename-agent" => { + self.open(Surface::Settings, cx); + self.palette_prev_focus = None; + self.agent_handle_input + .update(cx, |input, cx| input.focus(window, cx)); + } "copy" => { cx.write_to_clipboard(gpui::ClipboardItem::new_string( self.wallet_address_string(), diff --git a/crates/deckard-app/src/shell_chrome.rs b/crates/deckard-app/src/shell_chrome.rs index 1e91997..406f8bc 100644 --- a/crates/deckard-app/src/shell_chrome.rs +++ b/crates/deckard-app/src/shell_chrome.rs @@ -6,13 +6,13 @@ //! components — the demo scope is a single project / wallet, so the tree is a plain //! `v_flex` of rows. Color law (DESIGN §Color): ~95% grayscale; the selected row is a //! brightness lift (`secondary`), NEVER a colored keyline; amber is reserved for -//! Receive's keyline + focus rings; cyan appears ONLY on the agent squircle glyph -//! (in the wallet home's policy fence + the activity feed). +//! Receive's keyline + focus rings; cyan appears ONLY on the agent mark (the cyan +//! squircle, `widgets::agent_mark`) — the sidebar/breadcrumb agent, its surface, the feed. use gpui::prelude::FluentBuilder; use gpui::{ - div, px, AnyElement, Context, FontWeight, Hsla, InteractiveElement, IntoElement, ParentElement, - Pixels, StatefulInteractiveElement, Styled, + div, px, Context, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, + Styled, }; use gpui_component::{ button::{Button, ButtonVariants}, @@ -24,42 +24,10 @@ use crate::settings::ThemeModePref; use crate::shell::{Selection, Shell, Surface}; use crate::theme; -/// Middle-truncate an address for a tight row, e.g. `0xA1b2…9F3c`. Re-exported from -/// [`crate::widgets`] so existing call sites (`crate::shell_chrome::short_addr`) keep working -/// while the single canonical definition lives in the shared widget vocabulary. -pub(crate) use crate::widgets::short_addr; - -/// The agent's cyan squircle monogram — the ONE cyan surface (DESIGN §Actor model): a -/// rounded square (NEVER `rounded_full`) with the "A" monogram. Always static — the cyan -/// glyph is the two-signal identity marker; it carries no pulse or ambient motion. Shared -/// by the sidebar row, the wallet-home fence header, the palette, and the activity feed. -pub(crate) fn agent_squircle( - size: Pixels, - radius: Pixels, - agent: Hsla, - agent_tint: Hsla, -) -> AnyElement { - div() - .size(size) - .rounded(radius) - .bg(agent_tint) - .border_1() - .border_color(agent) - .flex() - .items_center() - .justify_center() - .child( - div() - .text_xs() - .font_weight(FontWeight::SEMIBOLD) - .text_color(agent) - .child("A"), - ) - .into_any_element() -} - impl Shell { - /// The current view's human label, for the breadcrumb's trailing segment. + /// The trailing view segment of the breadcrumb, for a full-pane action surface opened over the + /// selected wallet (`Meridian › Send`). `Home` has no trailing segment — its breadcrumb names + /// the focused entity alone (see [`Shell::breadcrumb_entity`]). fn view_label(&self) -> &'static str { match self.surface { Surface::Settings => "Settings", @@ -68,19 +36,26 @@ impl Shell { Surface::Shield => "Shield", Surface::Activity => "Activity", Surface::Swap => "Swap", - Surface::Home => match self.selection { - Selection::Project => "Personal", - Selection::Wallet => "Wallet", - Selection::Agent => "Atlas", - }, + Surface::Home => "", + } + } + + /// The entity the breadcrumb names (E2, #182): the agent handle on the agent home, "Personal" + /// on the project home, otherwise the wallet's name — the wallet is the entity every action + /// surface (Send/Receive/Shield/Swap/Settings) acts on. Drops the old `Personal ›` prefix and + /// the literal word Wallet. `is_agent` picks the cyan agent mark over the neutral identity mark. + fn breadcrumb_entity(&self) -> (String, bool) { + match (self.surface, self.selection) { + (Surface::Home, Selection::Agent) => (self.agent_handle(), true), + (Surface::Home, Selection::Project) => ("Personal".to_string(), false), + _ => (self.wallet_name(), false), } } - /// The hand-built sidebar tree: a PROJECTS label, one project row, a Wallets - /// group + one wallet row, a flex spacer, an Activity ledger row, and a footer - /// gear that opens Settings. Neutral throughout. (Atlas is key-less automation ON - /// the wallet — same EOA — so it lives in the wallet home's policy fence, not as a - /// separate sidebar entity.) + /// The hand-built sidebar tree: a PROJECTS label, one project row, a Wallets group + one + /// named wallet row, an Agents group + the first-class agent row (its cyan `agent_mark` + + /// handle + status), a flex spacer, an Activity ledger row, and a footer gear that opens + /// Settings. Neutral throughout except the agent's cyan mark. pub fn render_sidebar(&self, cx: &mut Context) -> impl IntoElement { let theme = cx.theme(); let fg = theme.foreground; @@ -104,7 +79,9 @@ impl Shell { let agent = theme::agent(is_dark); let agent_tint = theme::agent_tint(is_dark); - let addr = short_addr(&self.wallet_address_string()); + // Identity is named (E2, #182): the sidebar names the wallet + agent, not a raw address. + let wallet_name = self.wallet_name(); + let agent_handle = self.agent_handle(); let balance = self .portfolio .as_ref() @@ -146,8 +123,8 @@ impl Shell { .gap_2() .child(crate::widgets::identity_mark( "Personal", - px(16.0), - px(4.0), + crate::tokens::MARK_SM, + crate::tokens::RADIUS_ROW, id_square, fg, )) @@ -177,22 +154,24 @@ impl Shell { .gap_2() .min_w_0() .child(crate::widgets::identity_mark( - &addr, - px(16.0), - px(4.0), + &wallet_name, + crate::tokens::MARK_SM, + crate::tokens::RADIUS_ROW, id_square, fg, )) .child( div() - .font_family(mono.clone()) - .text_xs() + .min_w_0() + .truncate() + .text_sm() .text_color(fg) - .child(addr), + .child(wallet_name), ), ) .child( div() + .flex_shrink_0() .font_family(mono.clone()) .text_xs() .text_color(muted) @@ -217,8 +196,14 @@ impl Shell { h_flex() .items_center() .gap_2() - .child(agent_squircle(px(18.0), px(5.0), agent, agent_tint)) - .child(div().text_sm().text_color(fg).child("Atlas")) + .child(crate::widgets::agent_mark( + &agent_handle, + crate::tokens::MARK_SM, + crate::tokens::RADIUS_ROW, + agent, + agent_tint, + )) + .child(div().text_sm().text_color(fg).child(agent_handle)) .child(div().flex_1()) .child(div().size(px(6.0)).rounded_full().bg(agent)), ) @@ -281,16 +266,42 @@ impl Shell { ) } - /// The 44px breadcrumb bar: `[identity square] Personal › ` on the left, - /// and the neutral network pill + ⌘K affordance + theme toggle on the right - /// (the controls lifted out of the old title bar). + /// The 44px breadcrumb bar: `[mark] ` on the left — the entity the current view is + /// about (`Meridian`, the agent `Kyoto`, or `Personal`), plus `› ` on an action surface + /// (`Meridian › Send`). Identity is named (E2, #182): no `Personal ›` prefix, no literal Wallet. + /// The neutral network pill + ⌘K affordance + theme toggle sit on the right. pub fn render_breadcrumb(&self, cx: &mut Context) -> impl IntoElement { let theme = cx.theme(); let fg = theme.foreground; let muted = theme.muted_foreground; let border = theme.border; let surface = theme.secondary; - let id_square = theme::identity_square(theme.is_dark()); + let is_dark = theme.is_dark(); + let id_square = theme::identity_square(is_dark); + let agent = theme::agent(is_dark); + let agent_tint = theme::agent_tint(is_dark); + + // The focused entity + its mark (cyan agent mark vs. neutral identity mark). On an action + // surface the trailing `› ` is appended; Home shows the entity name alone. + let (entity_name, is_agent_entity) = self.breadcrumb_entity(); + let entity_mark = if is_agent_entity { + crate::widgets::agent_mark( + &entity_name, + crate::tokens::MARK_MD, + crate::tokens::RADIUS_ROW, + agent, + agent_tint, + ) + } else { + crate::widgets::identity_mark( + &entity_name, + crate::tokens::MARK_MD, + crate::tokens::RADIUS_ROW, + id_square, + fg, + ) + }; + let on_home = self.surface == Surface::Home; let theme_icon = if self.settings.theme_mode == ThemeModePref::Dark { IconName::Sun } else { @@ -316,23 +327,28 @@ impl Shell { h_flex() .items_center() .gap_2() - .child(crate::widgets::identity_mark( - "Personal", - px(16.0), - px(4.0), - id_square, - fg, - )) - .child(div().text_sm().text_color(fg).child("Personal")) - // Skip the trailing "› " when it would just repeat the project name - // (Project Home's label is "Personal" → avoid "Personal › Personal"). - .when( - !(self.surface == Surface::Home && self.selection == Selection::Project), - |el| { - el.child(div().text_sm().text_color(muted).child("›")) - .child(div().text_sm().text_color(fg).child(self.view_label())) - }, - ), + .min_w_0() + .child(entity_mark) + .child( + div() + .min_w_0() + .truncate() + .text_sm() + .text_color(fg) + .child(entity_name), + ) + // An action surface (Send/Receive/Shield/Swap/Activity/Settings) appends + // `› `; Home names the entity alone. + .when(!on_home, |el| { + el.child(div().flex_shrink_0().text_sm().text_color(muted).child("›")) + .child( + div() + .flex_shrink_0() + .text_sm() + .text_color(fg) + .child(self.view_label()), + ) + }), ) .child( h_flex() diff --git a/crates/deckard-app/src/swap.rs b/crates/deckard-app/src/swap.rs index ebb2df5..12ce041 100644 --- a/crates/deckard-app/src/swap.rs +++ b/crates/deckard-app/src/swap.rs @@ -185,7 +185,7 @@ pub fn confirm_swap_blocking( // (3) Propose the order FIRST. A valid order is always NeedsApproval; a Deny is terminal. The // pending order is also what admits the exact-gross approve in step 4. App-origin: this is - // the user's foreground GUI swap, so the feed labels it "You", not "Atlas". + // the user's foreground GUI swap, so the feed labels it "You", not the agent handle. match client.propose_order_blocking(&order, deckard_contract::ProposalOrigin::App)? { Decision::NeedsApproval { .. } => {} Decision::Allow => { diff --git a/crates/deckard-app/src/tokens.rs b/crates/deckard-app/src/tokens.rs index 48dd7a4..3614daf 100644 --- a/crates/deckard-app/src/tokens.rs +++ b/crates/deckard-app/src/tokens.rs @@ -45,6 +45,8 @@ pub const STROKE_HAIRLINE: Pixels = px(1.0); // first consumer (an unused `pub const` is a build error here), so `mark-sm` (16, the inline // row/chip mark) arrives with E2/E6 when a 16px mark does; E1 needs only these two rungs. A // mark picks the nearest rung, never a raw off-ladder `px()`. ── +/// An inline identity mark in a row or chip (e.g. the ⌘K scope line's wallet/agent mark). +pub const MARK_SM: Pixels = px(16.0); /// A balance-diff token mark (sidebar / breadcrumb marks are this rung too). pub const MARK_MD: Pixels = px(20.0); /// The page-header mark + the shared-Review origin / metadata-rail object mark. diff --git a/crates/deckard-app/src/welcome.rs b/crates/deckard-app/src/welcome.rs index 30c4518..95ad479 100644 --- a/crates/deckard-app/src/welcome.rs +++ b/crates/deckard-app/src/welcome.rs @@ -17,9 +17,10 @@ use deckard_core::{tokens_for, U256}; use crate::money::money; use crate::shell::{Shell, Surface}; -use crate::shell_chrome::agent_squircle; use crate::theme; -use crate::widgets::{budget_gauge, identity_mark, section_label, short_addr}; +use crate::widgets::{ + agent_mark, budget_gauge, identity_mark, page_header, section_label, short_addr, +}; /// One row in the holdings table. Carries the raw balance (not a pre-formatted /// string) so the amount column can render mono-for-money with dimmed decimals. @@ -148,14 +149,11 @@ impl Shell { .map(|p| !p.tokens.is_empty()) .unwrap_or(false); - // Wallet identity for the header: a desaturated, tinted-neutral square - // (DESIGN rule 4 — identity squares avoid the warm/amber band). + // Wallet identity for the masthead: a desaturated, tinted-neutral square + // (DESIGN rule 4 — identity squares avoid the warm/amber band). Identity is named + // (E2, #182): the real wallet name, never the literal word Wallet. let id_square = theme::identity_square(theme.is_dark()); - let wallet_name = if self.viewing_watch { - "Watched account".to_string() - } else { - "Personal".to_string() - }; + let wallet_name = self.wallet_name(); div() .size_full() @@ -168,51 +166,40 @@ impl Shell { .items_start() .max_w(px(680.0)) .gap_6() - // Page header (DESIGN §Page header): identity square + wallet-name - // H1 (text.primary, weight 600 — NEVER cyan) + a muted mono, - // middle-truncated address subtitle. + // Identity masthead (DESIGN §request-origin model: name + mark above the mono + // hero) via the one shared `page_header` anatomy: a rounded identity square + + // the wallet-name H1 (text.primary, 600 — NEVER cyan) + the muted mono, + // middle-truncated address subtitle. A ghost Refresh caps the row. .child( h_flex() .w_full() .items_center() - .justify_between() - .child( - h_flex() - .items_center() - .gap_3() - .child(identity_mark( - &wallet_name, - px(28.0), - px(6.0), - id_square, - fg, - )) - .child( - v_flex() - .gap_0p5() - .child( - div() - .text_xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(fg) - .child(wallet_name), - ) - .child( - div() - .font_family(mono.clone()) - .text_xs() - .text_color(muted) - .child(account_pill), - ), - ), - ) + .gap_3() + .child(div().flex_1().min_w_0().child(page_header( + identity_mark( + &wallet_name, + crate::tokens::MARK_LG, + crate::tokens::RADIUS_ROW, + id_square, + fg, + ), + &wallet_name, + Some(account_pill.as_str()), + Some(mono.clone()), + fg, + muted, + ))) .child( - Button::new("refresh") - .ghost() - .icon(IconName::Replace) - .on_click( - cx.listener(|this, _, _, cx| this.refresh_portfolio(cx)), - ), + div().flex_shrink_0().child( + Button::new("refresh") + .ghost() + .icon(IconName::Replace) + .on_click( + cx.listener(|this, _, _, cx| { + this.refresh_portfolio(cx) + }), + ), + ), ), ) // Balance hero: the merged Total (public + private), a Private/Public @@ -265,21 +252,21 @@ impl Shell { ) // Holdings, or a state. .child(self.render_holdings(first_sync, has_tokens, holdings, cx)) - // Compact agent presence — ONE clickable Atlas row that opens the agent - // surface (where the full policy fence now lives). Atlas is key-less + // Compact agent presence — ONE clickable agent row that opens the agent + // surface (where the full policy fence now lives). The agent is key-less // automation ON this same wallet (same EOA); the home only carries a calm // status + budget glance, not the full configuration. .child(self.render_agent_presence(cx)), ) } - /// The compact agent presence for the wallet home — a single clickable Atlas row that + /// The compact agent presence for the wallet home — a single clickable agent row that /// navigates to the agent surface (`Selection::Agent`), where the full policy fence lives. /// Set off from the holdings above by whitespace + a top hairline (editorial section, NOT a - /// card). The row carries the cyan identity squircle, the "Atlas" name, a small "acting" cyan - /// status, and a thin Spent-today/Daily-budget gauge (from the daemon's LIVE policy, never - /// invented) in a ~200px container on the right, plus a muted chevron. When the policy hasn't - /// landed yet (`agent_policy == None`) the row reads just "Atlas · idle" with no gauge. + /// card). The row carries the cyan `agent_mark` (handle-seeded), the agent handle, a small + /// "acting" cyan status, and a thin Spent-today/Daily-budget gauge (from the daemon's LIVE + /// policy, never invented) in a ~200px container on the right, plus a muted chevron. When the + /// policy hasn't landed yet (`agent_policy == None`) the row reads just " · idle". fn render_agent_presence(&self, cx: &mut Context) -> impl IntoElement { let theme = cx.theme(); let fg = theme.foreground; @@ -291,6 +278,7 @@ impl Shell { let agent = theme::agent(is_dark); let agent_tint = theme::agent_tint(is_dark); let amber = theme::amber(is_dark); + let agent_handle = self.agent_handle(); // The Spent-today / Daily-budget gauge in a ~200px container, computed from the live policy // (never invented). `frac = spent_today / daily_cap` via the integer-safe `fraction` helper; @@ -335,13 +323,19 @@ impl Shell { .gap_3() .py_1p5() .cursor_pointer() - .child(agent_squircle(px(18.0), px(5.0), agent, agent_tint)) + .child(agent_mark( + &agent_handle, + crate::tokens::MARK_MD, + crate::tokens::RADIUS_ROW, + agent, + agent_tint, + )) .child( div() .text_sm() .font_weight(FontWeight::SEMIBOLD) .text_color(fg) - .child("Atlas"), + .child(agent_handle), ) // Status: "acting" (cyan) when the policy is live, a muted "idle" otherwise. .child(if has_policy { diff --git a/crates/deckard-app/src/widgets.rs b/crates/deckard-app/src/widgets.rs index cc30460..3aef1b6 100644 --- a/crates/deckard-app/src/widgets.rs +++ b/crates/deckard-app/src/widgets.rs @@ -8,7 +8,7 @@ //! screen *cannot* drift. //! //! Style matches the rest of the crate: the atomic primitives are pure functions that take -//! explicit theme colors (`Hsla`) and return an `AnyElement` (see `shell_chrome::agent_squircle`). +//! explicit theme colors (`Hsla`) and return an `AnyElement` (see `identity_mark` / `agent_mark`). //! No raw hex; callers pass `cx.theme().*` / `theme::amber(is_dark)`. //! //! The v4 *composite* widgets (`origin_header`, `status_glyph`, `balance_diff`, the `meta_rail` @@ -118,7 +118,7 @@ fn monogram(seed: &str) -> SharedString { /// §Actor model: shape is the accessibility backup; never a blank fill). Square /// (rounded) for projects/wallets; pass `radius = size / 2` for the round human /// principal. `fill` is the desaturated identity slate; `glyph` tints the monogram. -/// The agent uses [`agent_mark`] / `shell_chrome::agent_squircle` instead (cyan, the actor signal). +/// The agent uses [`agent_mark`] instead (cyan-bordered, the actor signal). pub(crate) fn identity_mark( seed: &str, size: Pixels, @@ -144,14 +144,11 @@ pub(crate) fn identity_mark( } /// The **cyan agent squircle**, handle-aware (DESIGN §Actor model: agent = a cyan squircle monogram -/// — the ONE cyan surface). Same treatment as `shell_chrome::agent_squircle` — cyan-tint fill + the -/// **cyan border that defines the squircle** + a cyan monogram — but seeded so it renders the agent's -/// handle initial (`K` for `Kyoto`) instead of a fixed `A`. The bordered cyan mark IS the two-signal -/// actor signal, so it keeps the border `identity_mark` omits. -// reason: consumed by `origin_header` (E5, #185); E2 (#182) migrates the sidebar/breadcrumb/feed off -// the fixed-`A` `shell_chrome::agent_squircle` onto this handle-aware widget. -#[allow(dead_code)] -fn agent_mark( +/// — the ONE cyan surface): a cyan-tint fill + the **cyan border that defines the squircle** + a +/// cyan monogram, seeded so it renders the agent's handle initial (`K` for `Kyoto`). The bordered +/// cyan mark IS the two-signal actor signal, so it keeps the border `identity_mark` omits. The one +/// agent mark for the sidebar, breadcrumb, wallet-home presence, agent surface, and activity feed. +pub(crate) fn agent_mark( seed: &str, size: Pixels, radius: Pixels, @@ -496,15 +493,17 @@ pub(crate) fn kv_row( } /// The ONE page-header anatomy (DESIGN §Component primitives): a caller-built identity `mark` -/// (`identity_mark` / `agent_squircle`) + an H1 title at ONE size (`text_xl`, `text.primary`, -/// 600) + an optional muted one-line subtitle. Kills the hand-rolled headers at three sizes. -// reason: consumed by the v4 view headers (E2/E4/E6/E7); E1 lands the one anatomy so no view -// re-rolls a header at its own size. -#[allow(dead_code)] +/// (`identity_mark` / `agent_mark`) + an H1 title at ONE size (`text_xl`, `text.primary`, 600) + +/// an optional muted one-line subtitle. Kills the hand-rolled headers at three sizes. Pass +/// `subtitle_mono = Some(theme.mono_font_family)` when the subtitle is an address (DESIGN §Trust: +/// addresses are mono), `None` for prose. +// The one header anatomy consumed by the v4 view headers (E2 wires the wallet-home masthead; +// E4/E6/E7 follow) so no view re-rolls a header at its own size. pub(crate) fn page_header( mark: AnyElement, title: &str, subtitle: Option<&str>, + subtitle_mono: Option, primary: Hsla, muted: Hsla, ) -> AnyElement { @@ -527,8 +526,11 @@ pub(crate) fn page_header( .when_some(subtitle, |d, s| { d.child( div() + .min_w_0() + .truncate() .text_sm() .text_color(muted) + .when_some(subtitle_mono, |d, mono| d.font_family(mono)) .child(SharedString::from(s.to_string())), ) }),