feat(hololake): implement GHS-014 stage-one home

This commit is contained in:
冰朔 2026-08-13 13:52:59 +08:00
commit 4d2415a35b
15 changed files with 891 additions and 49 deletions

View file

@ -5,6 +5,7 @@
*.tsbuildinfo *.tsbuildinfo
/vite.config.js /vite.config.js
/vite.config.d.ts /vite.config.d.ts
/work/
*.key *.key
*.p12 *.p12
*.pfx *.pfx

View file

@ -0,0 +1,74 @@
{
"schema": "hololake.ghs-014-stage-one-ui-receipt/v1",
"receipt_id": "GHS-014-STAGE-ONE-UI-20260813-001",
"state": "IMPLEMENTED_AND_VISUALLY_VERIFIED",
"source": {
"repository": "REPO-012",
"verified_main_commit": "457bf99b3a2af8b97290ed71ca3515e599fc17ae",
"brain_entry": {
"path": "skills/shared/guanghu-client-ui-brain/BRAIN.hdlp",
"sha256": "b895b7f0eb8af6125b25caaa90c18fde966d884bbe1dec661d5bdfe08ff4e97a"
},
"five_lakes_tokens": {
"path": "skills/shared/guanghu-client-ui-brain/references/theme-tokens-five-lakes.md",
"sha256": "563f6ce6afe665deb52af64671d0a31b9673c120a596ea7ff34548fe7359b5a8"
},
"locked_home_style": {
"path": "skills/shared/guanghu-client-ui-brain/references/home-screen-style-locked-v1.2.md",
"sha256": "23f0ab6655cd128de2956f28451fead7b78b0a0c920fb0497efba312299ef167"
}
},
"stage_one_correction": {
"visible_five_domain_gates": false,
"reason": "The current human decision submerges all five domains as dynamic routed infrastructure during stage one.",
"visible_focus": "PERSONAL_CHANNEL_EXTERNAL_PROGRAMMING_AI_ENTRY"
},
"implemented": {
"theme_ids": ["night", "dawn", "nebula", "candle", "clear"],
"theme_names": ["夜湖星光", "晨湖曦光", "星云紫夜", "烛畔暖湖", "清浅澄湖"],
"theme_changes_tokens_only": true,
"primary_window_verified": "1180x760",
"compact_window_verified": "800x600",
"hard_status_dividers_present": false,
"native_home_status_projection": true,
"false_connected_state_allowed": false
},
"concept_to_render_fidelity_ledger": [
{
"point": "lake atmosphere and horizon",
"result": "PRESERVED",
"note": "Night lake, mist, star fragments and warm horizon remain the visual field."
},
{
"point": "single primary focus",
"result": "PRESERVED_AND_SIMPLIFIED",
"note": "The personal-channel language entry is the only above-fold product focus."
},
{
"point": "connection state",
"result": "CORRECTED_TO_RUNTIME_TRUTH",
"note": "The concept sample said connected; the product says waiting, resumable or connected from native state."
},
{
"point": "status separation",
"result": "CORRECTED_TO_GHS_014",
"note": "Concept divider lines were removed in favor of spacing and soft light."
},
{
"point": "update state",
"result": "CORRECTED_TO_FAIL_CLOSED_TRUTH",
"note": "Unprovisioned release trust is shown as networking disabled, never as current/latest."
}
],
"above_fold_copy_diff": {
"concept": ["光湖", "个人频道", "语言进入这里,成为可见、可续的现实。"],
"render": ["光湖", "个人频道", "语言进入这里,成为可见、可续的现实。"],
"difference": "NONE"
},
"verification": {
"browser_visual_inspection": "PASS",
"semantic_dom_inspection": "PASS",
"five_theme_invariant_check": "PASS",
"latest_render_visually_compared_with_concept": true
}
}

View file

@ -2,6 +2,10 @@
The stage-one shell is a Tauri v2 application with a Rust-owned local core and a React human projection. The five domains and their server fleet remain submerged system infrastructure rather than primary stage-one navigation. The stage-one shell is a Tauri v2 application with a Rust-owned local core and a React human projection. The five domains and their server fleet remain submerged system infrastructure rather than primary stage-one navigation.
## Stage-one human projection
The home surface follows the verified GHS-014 five-lakes visual grammar while applying the current stage-one correction: it exposes one personal-channel entry, not five domain gates. The five named lake themes are token groups only and cannot change layout, copy, routing or authority. The surface reads a native status projection; only an authenticated direct connector can illuminate the live-connected state. A stored resumable session remains visibly distinct from a live connection.
## External programming AI entry ## External programming AI entry
MCP may discover HoloLake, but it does not own continuity. The installed application starts a user-only Unix socket broker. A programming AI opens or resumes a HoloLake-issued local session, then uses the installed executable's `--connector` mode for newline-delimited protocol traffic. Session secrets are stored only as hashes. Events use exact cursors and idempotency keys. MCP may discover HoloLake, but it does not own continuity. The installed application starts a user-only Unix socket broker. A programming AI opens or resumes a HoloLake-issued local session, then uses the installed executable's `--connector` mode for newline-delimited protocol traffic. Session secrets are stored only as hashes. Events use exact cursors and idempotency keys.

View file

@ -1,10 +1,15 @@
{ {
"schema": "hololake.native-desktop-foundation/v1", "schema": "hololake.native-desktop-foundation/v1",
"record_id": "HLP-NATIVE-DESKTOP-FOUNDATION-001", "record_id": "HLP-NATIVE-DESKTOP-FOUNDATION-001",
"state": "TAURI_FOUNDATION_WITH_DIRECT_LOCAL_BROKER_DYNAMIC_ROUTING_AND_PNCC_READ_ONLY_CORE", "state": "TAURI_STAGE_ONE_HOME_WITH_DIRECT_LOCAL_BROKER_DYNAMIC_ROUTING_AND_PNCC_READ_ONLY_CORE",
"canonical_shell": "TAURI_V2_RUST_REACT", "canonical_shell": "TAURI_V2_RUST_REACT",
"product_ui_implementation_started": false, "product_ui_implementation_started": true,
"selected_visual_direction_present": false, "selected_visual_direction_present": true,
"selected_visual_direction": "GHS-014_FIVE_LAKES_PERSONAL_CHANNEL_STAGE_ONE",
"selected_visual_direction_receipt": "audit/ghs-014-stage-one-ui-receipt.json",
"home_status_is_native_projection": true,
"home_online_state_requires_authenticated_direct_connection": true,
"five_lake_themes_implemented": true,
"upstream_product_update_endpoints": [], "upstream_product_update_endpoints": [],
"production_update_endpoint_owner": "HOLOLAKE_ONLY", "production_update_endpoint_owner": "HOLOLAKE_ONLY",
"release_broadcast_contract": "contracts/release-broadcast.schema.json", "release_broadcast_contract": "contracts/release-broadcast.schema.json",
@ -37,7 +42,7 @@
"pncc_receipt_projection_implemented": true, "pncc_receipt_projection_implemented": true,
"pncc_authenticated_direct_broker_integration_implemented": true, "pncc_authenticated_direct_broker_integration_implemented": true,
"pncc_human_mount_registration_implemented": false, "pncc_human_mount_registration_implemented": false,
"pncc_human_mount_registration_gate": "NATIVE_FILE_PICKER_EXACT_CONFIRMATION_AND_UI_ACCEPTANCE_REQUIRED", "pncc_human_mount_registration_gate": "NATIVE_FILE_PICKER_EXACT_CONFIRMATION_REQUIRED",
"pncc_internal_model_inference_implemented": false, "pncc_internal_model_inference_implemented": false,
"pncc_execution_limb_implemented": false, "pncc_execution_limb_implemented": false,
"pncc_persona_lease_acquisition_implemented": false, "pncc_persona_lease_acquisition_implemented": false,

View file

@ -27,7 +27,7 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::os::unix::net::{UnixListener, UnixStream}; use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, AtomicUsize, Ordering},
mpsc, Arc, mpsc, Arc,
}; };
use std::thread; use std::thread;
@ -40,10 +40,33 @@ const MAX_REQUEST_BYTES: u64 = 1024 * 1024;
pub struct DirectLocalBrokerHandle { pub struct DirectLocalBrokerHandle {
shutdown: Arc<AtomicBool>, shutdown: Arc<AtomicBool>,
authenticated_connections: Arc<AtomicUsize>,
socket_path: PathBuf, socket_path: PathBuf,
worker: Option<thread::JoinHandle<()>>, worker: Option<thread::JoinHandle<()>>,
} }
impl DirectLocalBrokerHandle {
pub fn active_connection_count(&self) -> usize {
self.authenticated_connections.load(Ordering::Acquire)
}
}
struct AuthenticatedConnectionGuard(Arc<AtomicUsize>);
impl Drop for AuthenticatedConnectionGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::AcqRel);
}
}
struct BrokerStorageRoots {
session: PathBuf,
routing: PathBuf,
pncc_mount: PathBuf,
pncc_remote: PathBuf,
pncc_projection: PathBuf,
}
impl Drop for DirectLocalBrokerHandle { impl Drop for DirectLocalBrokerHandle {
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown.store(true, Ordering::Release); self.shutdown.store(true, Ordering::Release);
@ -296,7 +319,9 @@ fn start_at(
)?; )?;
let shutdown = Arc::new(AtomicBool::new(false)); let shutdown = Arc::new(AtomicBool::new(false));
let authenticated_connections = Arc::new(AtomicUsize::new(0));
let worker_shutdown = Arc::clone(&shutdown); let worker_shutdown = Arc::clone(&shutdown);
let worker_authenticated_connections = Arc::clone(&authenticated_connections);
let worker_socket = socket_path.clone(); let worker_socket = socket_path.clone();
let (ready_sender, ready_receiver) = mpsc::sync_channel(1); let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
let worker = thread::Builder::new() let worker = thread::Builder::new()
@ -305,12 +330,15 @@ fn start_at(
let _ = ready_sender.send(()); let _ = ready_sender.send(());
serve( serve(
listener, listener,
session_root, BrokerStorageRoots {
routing_root, session: session_root,
pncc_mount_root, routing: routing_root,
pncc_remote_root, pncc_mount: pncc_mount_root,
pncc_projection_root, pncc_remote: pncc_remote_root,
pncc_projection: pncc_projection_root,
},
&worker_shutdown, &worker_shutdown,
&worker_authenticated_connections,
); );
let _ = fs::remove_file(worker_socket); let _ = fs::remove_file(worker_socket);
}) })
@ -321,6 +349,7 @@ fn start_at(
Ok(DirectLocalBrokerHandle { Ok(DirectLocalBrokerHandle {
shutdown, shutdown,
authenticated_connections,
socket_path, socket_path,
worker: Some(worker), worker: Some(worker),
}) })
@ -328,12 +357,9 @@ fn start_at(
fn serve( fn serve(
listener: UnixListener, listener: UnixListener,
session_root: PathBuf, roots: BrokerStorageRoots,
routing_root: PathBuf,
pncc_mount_root: PathBuf,
pncc_remote_root: PathBuf,
pncc_projection_root: PathBuf,
shutdown: &AtomicBool, shutdown: &AtomicBool,
authenticated_connections: &Arc<AtomicUsize>,
) { ) {
while !shutdown.load(Ordering::Acquire) { while !shutdown.load(Ordering::Acquire) {
match listener.accept() { match listener.accept() {
@ -341,11 +367,15 @@ fn serve(
if shutdown.load(Ordering::Acquire) { if shutdown.load(Ordering::Acquire) {
break; break;
} }
let root = session_root.clone(); if stream.set_nonblocking(false).is_err() {
let routes = routing_root.clone(); continue;
let pncc_mounts = pncc_mount_root.clone(); }
let pncc_remote = pncc_remote_root.clone(); let root = roots.session.clone();
let pncc_projection = pncc_projection_root.clone(); let routes = roots.routing.clone();
let pncc_mounts = roots.pncc_mount.clone();
let pncc_remote = roots.pncc_remote.clone();
let pncc_projection = roots.pncc_projection.clone();
let client_authenticated_connections = Arc::clone(authenticated_connections);
let _ = thread::Builder::new() let _ = thread::Builder::new()
.name("hololake-direct-local-client".into()) .name("hololake-direct-local-client".into())
.spawn(move || { .spawn(move || {
@ -356,6 +386,7 @@ fn serve(
&pncc_mounts, &pncc_mounts,
&pncc_remote, &pncc_remote,
&pncc_projection, &pncc_projection,
client_authenticated_connections,
) )
}); });
} }
@ -374,12 +405,14 @@ fn serve_connection(
pncc_mount_root: &Path, pncc_mount_root: &Path,
pncc_remote_root: &Path, pncc_remote_root: &Path,
pncc_projection_root: &Path, pncc_projection_root: &Path,
authenticated_connections: Arc<AtomicUsize>,
) { ) {
let read_stream = match stream.try_clone() { let read_stream = match stream.try_clone() {
Ok(stream) => stream, Ok(stream) => stream,
Err(_) => return, Err(_) => return,
}; };
let mut reader = BufReader::new(read_stream); let mut reader = BufReader::new(read_stream);
let mut authenticated_connection = None;
loop { loop {
let mut bytes = Vec::new(); let mut bytes = Vec::new();
let read = match reader let read = match reader
@ -393,6 +426,7 @@ fn serve_connection(
if read == 0 { if read == 0 {
return; return;
} }
let request_authenticates = request_establishes_authenticated_connection(&bytes);
let response = if read as u64 > MAX_REQUEST_BYTES || !bytes.ends_with(b"\n") { let response = if read as u64 > MAX_REQUEST_BYTES || !bytes.ends_with(b"\n") {
BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE") BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE")
} else { } else {
@ -405,6 +439,12 @@ fn serve_connection(
&bytes[..bytes.len() - 1], &bytes[..bytes.len() - 1],
) )
}; };
if response.ok && request_authenticates && authenticated_connection.is_none() {
authenticated_connections.fetch_add(1, Ordering::AcqRel);
authenticated_connection = Some(AuthenticatedConnectionGuard(Arc::clone(
&authenticated_connections,
)));
}
if serde_json::to_writer(&mut stream, &response).is_err() if serde_json::to_writer(&mut stream, &response).is_err()
|| stream.write_all(b"\n").is_err() || stream.write_all(b"\n").is_err()
|| stream.flush().is_err() || stream.flush().is_err()
@ -417,6 +457,18 @@ fn serve_connection(
} }
} }
fn request_establishes_authenticated_connection(bytes: &[u8]) -> bool {
serde_json::from_slice::<serde_json::Value>(bytes)
.ok()
.and_then(|value| {
value
.get("operation")
.and_then(Value::as_str)
.map(str::to_owned)
})
.is_some_and(|operation| matches!(operation.as_str(), "OPEN_SESSION" | "RESUME_SESSION"))
}
fn dispatch( fn dispatch(
session_root: &Path, session_root: &Path,
routing_root: &Path, routing_root: &Path,
@ -840,4 +892,68 @@ mod tests {
assert_eq!(verified["result"]["emptyMeansOffline"], false); assert_eq!(verified["result"]["emptyMeansOffline"], false);
assert_eq!(verified["result"]["returnedEventCount"], 0); assert_eq!(verified["result"]["returnedEventCount"], 0);
} }
#[test]
fn only_an_authenticated_persistent_connector_counts_as_online() {
let temp = TempDir::new().unwrap();
let socket = temp.path().join("runtime/broker.sock");
let descriptor = temp.path().join("broker.json");
let sessions = temp.path().join("sessions");
let routes = temp.path().join("routes");
fs::create_dir_all(&sessions).unwrap();
fs::create_dir_all(&routes).unwrap();
let broker = start_at(sessions.clone(), routes, descriptor, socket.clone()).unwrap();
let probe = UnixStream::connect(&socket).unwrap();
thread::sleep(Duration::from_millis(50));
assert_eq!(broker.active_connection_count(), 0);
drop(probe);
let ticket = issue_ticket_at(
&sessions,
IssueDiscoveryTicketInput {
account_id: "human-1".into(),
lane_id: "DEV-1".into(),
client_instance_id: "codex-1".into(),
},
)
.unwrap();
let mut connector = UnixStream::connect(&socket).unwrap();
serde_json::to_writer(
&mut connector,
&serde_json::json!({
"operation": "OPEN_SESSION",
"input": {
"accountId": "human-1",
"laneId": "DEV-1",
"clientInstanceId": "codex-1",
"discoveryTicket": ticket.discovery_ticket
}
}),
)
.unwrap();
connector.write_all(b"\n").unwrap();
assert!(request_establishes_authenticated_connection(
br#"{"operation":"OPEN_SESSION"}"#
));
let mut response = String::new();
let mut response_reader = BufReader::new(connector.try_clone().unwrap());
response_reader.read_line(&mut response).unwrap();
assert_eq!(
serde_json::from_str::<Value>(&response).unwrap()["ok"],
true
);
thread::sleep(Duration::from_millis(50));
assert_eq!(broker.active_connection_count(), 1);
drop(connector);
drop(response_reader);
for _ in 0..20 {
if broker.active_connection_count() == 0 {
break;
}
thread::sleep(Duration::from_millis(10));
}
assert_eq!(broker.active_connection_count(), 0);
}
} }

View file

@ -203,6 +203,43 @@ pub(crate) fn direct_session_root(app: &AppHandle) -> Result<PathBuf, String> {
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}")) .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))
} }
pub(crate) fn active_session_count_at(root: &Path) -> Result<usize, String> {
let accounts = root.join("accounts");
if !accounts.exists() {
return Ok(0);
}
let mut count = 0;
for entry in fs::read_dir(&accounts)
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?
{
let entry = entry
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
let kind = entry
.file_type()
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
if !kind.is_dir() || kind.is_symlink() {
continue;
}
let active_path = entry.path().join("active-session.json");
if !active_path.exists() {
continue;
}
let metadata = fs::symlink_metadata(&active_path)
.map_err(|error| format!("HOLOLAKE_ACTIVE_SESSION_INVALID: {error}"))?;
if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
return Err("HOLOLAKE_ACTIVE_SESSION_INVALID".into());
}
let active: ActiveSessionRecord = read_json(&active_path, "ACTIVE_SESSION")?;
if active.schema != SESSION_SCHEMA
|| active.account_key != entry.file_name().to_string_lossy()
{
return Err("HOLOLAKE_ACTIVE_SESSION_INVALID".into());
}
count += 1;
}
Ok(count)
}
pub(crate) fn open_at( pub(crate) fn open_at(
root: &Path, root: &Path,
input: OpenSessionInput, input: OpenSessionInput,

View file

@ -0,0 +1,44 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::direct_local_broker::DirectLocalBrokerHandle;
use crate::direct_local_session::{active_session_count_at, direct_session_root};
use crate::pncc_receipt_projection::{pncc_projection_root, projection_event_count_at};
use crate::pncc_repository_binding::{mounted_repository_count_at, pncc_repository_mount_root};
use crate::release_trust::release_trust_state;
use serde::Serialize;
use tauri::{AppHandle, State};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HoloLakeHomeStatus {
pub schema: &'static str,
pub direct_local_broker_state: &'static str,
pub direct_connection_count: usize,
pub resumable_session_count: usize,
pub code_repository_mount_count: usize,
pub pncc_receipt_count: usize,
pub update_state: &'static str,
pub automatic_upstream_updates: bool,
pub mcp_role: &'static str,
}
#[tauri::command]
pub fn get_hololake_home_status(
app: AppHandle,
broker: State<'_, DirectLocalBrokerHandle>,
) -> Result<HoloLakeHomeStatus, String> {
let session_root = direct_session_root(&app)?;
let mount_root = pncc_repository_mount_root(&app)?;
let projection_root = pncc_projection_root(&app)?;
Ok(HoloLakeHomeStatus {
schema: "hololake.home-status/v1",
direct_local_broker_state: "READY",
direct_connection_count: broker.active_connection_count(),
resumable_session_count: active_session_count_at(&session_root)?,
code_repository_mount_count: mounted_repository_count_at(&mount_root)?,
pncc_receipt_count: projection_event_count_at(&projection_root)?,
update_state: release_trust_state()?,
automatic_upstream_updates: false,
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
})
}

View file

@ -1,6 +1,7 @@
mod direct_local_broker; mod direct_local_broker;
mod direct_local_session; mod direct_local_session;
mod dynamic_capability_routing; mod dynamic_capability_routing;
mod home_status;
mod local_development_bridge; mod local_development_bridge;
mod pncc_receipt_projection; mod pncc_receipt_projection;
mod pncc_remote_git; mod pncc_remote_git;
@ -17,6 +18,7 @@ pub fn run_connector() -> Result<(), String> {
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
home_status::get_hololake_home_status,
direct_local_session::issue_direct_local_discovery_ticket, direct_local_session::issue_direct_local_discovery_ticket,
direct_local_session::open_direct_local_session, direct_local_session::open_direct_local_session,
direct_local_session::resume_direct_local_session, direct_local_session::resume_direct_local_session,

View file

@ -102,6 +102,11 @@ pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result<PathBuf, String> {
.map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}")) .map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))
} }
pub(crate) fn projection_event_count_at(root: &Path) -> Result<usize, String> {
let _lock = lock_projection(root)?;
Ok(load_and_verify(root)?.events.len())
}
pub(crate) fn append_repository_binding_at( pub(crate) fn append_repository_binding_at(
root: &Path, root: &Path,
receipt: &PnccRepositoryMountReceipt, receipt: &PnccRepositoryMountReceipt,

View file

@ -77,6 +77,32 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, Str
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}")) .map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))
} }
pub(crate) fn mounted_repository_count_at(root: &Path) -> Result<usize, String> {
let mut count = 0;
for entry in fs::read_dir(root)
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?
{
let entry =
entry.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
let kind = entry
.file_type()
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
let path = entry.path();
if kind.is_symlink()
|| !kind.is_file()
|| path.extension().and_then(|value| value.to_str()) != Some("json")
{
continue;
}
let record: PnccRepositoryMountRecord = read_json(&path)?;
if record.schema != MOUNT_SCHEMA || mount_path(root, &record.mount_id) != path {
return Err("PNCC_REPOSITORY_MOUNT_RECORD_INVALID".into());
}
count += 1;
}
Ok(count)
}
pub(crate) fn inspect_mounted_at( pub(crate) fn inspect_mounted_at(
root: &Path, root: &Path,
input: InspectMountedPnccRepositoryInput, input: InspectMountedPnccRepositoryInput,

View file

@ -33,6 +33,13 @@ enum ValidatedReleaseTrust {
}, },
} }
pub(crate) fn release_trust_state() -> Result<&'static str, String> {
match validate_release_trust(EMBEDDED_RELEASE_TRUST)? {
ValidatedReleaseTrust::Disabled => Ok("UNPROVISIONED_FAIL_CLOSED"),
ValidatedReleaseTrust::Enabled { .. } => Ok("READY_HUMAN_CONFIRMATION_REQUIRED"),
}
}
fn validate_release_trust(raw: &str) -> Result<ValidatedReleaseTrust, String> { fn validate_release_trust(raw: &str) -> Result<ValidatedReleaseTrust, String> {
let trust: ReleaseTrust = serde_json::from_str(raw) let trust: ReleaseTrust = serde_json::from_str(raw)
.map_err(|error| format!("invalid HoloLake release trust document: {error}"))?; .map_err(|error| format!("invalid HoloLake release trust document: {error}"))?;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -0,0 +1,167 @@
:root,
[data-theme='night'] {
/* Primitive tokens — GHS-014 five-lakes v1.3, night lake. */
--primitive-bg-a: #0a1020;
--primitive-bg-b: #0b1220;
--primitive-bg-c: #070c16;
--primitive-bg-d: #04070d;
--primitive-star-core: #f7ebc8;
--primitive-star-bright: #ffefc2;
--primitive-text-primary: rgba(234, 238, 248, 0.96);
--primitive-text-body: rgba(222, 228, 242, 0.88);
--primitive-text-aux: rgba(196, 206, 228, 0.62);
--primitive-text-faint: rgba(180, 192, 220, 0.4);
--primitive-nebula-a: rgba(91, 111, 196, 0.13);
--primitive-nebula-b: rgba(138, 111, 201, 0.05);
--primitive-glass: rgba(178, 198, 255, 0.045);
--primitive-glass-hover: rgba(178, 198, 255, 0.075);
--primitive-glass-edge: rgba(202, 216, 255, 0.13);
--primitive-glass-top: rgba(255, 255, 255, 0.06);
--primitive-mist-a: rgba(157, 180, 255, 0.1);
--primitive-mist-b: rgba(247, 235, 200, 0.04);
--primitive-cool-glow: rgba(157, 180, 255, 0.42);
--primitive-warm-glow: rgba(247, 235, 200, 0.5);
--primitive-shadow: rgba(0, 0, 0, 0.34);
--primitive-scene-opacity: 0.76;
/* Semantic tokens. */
--surface-sky: var(--primitive-bg-a);
--surface-horizon: var(--primitive-bg-b);
--surface-lake: var(--primitive-bg-c);
--surface-depth: var(--primitive-bg-d);
--content-primary: var(--primitive-text-primary);
--content-secondary: var(--primitive-text-body);
--content-muted: var(--primitive-text-aux);
--content-faint: var(--primitive-text-faint);
--accent-light: var(--primitive-star-core);
--accent-light-strong: var(--primitive-star-bright);
--state-ready: #b8d7c4;
--state-waiting: #cfbddf;
--state-quiet: var(--primitive-text-aux);
--focus-ring: rgba(247, 235, 200, 0.7);
/* Component tokens. */
--button-primary-bg: rgba(247, 235, 200, 0.92);
--button-primary-text: #15181f;
--button-primary-shadow: 0 12px 42px rgba(247, 235, 200, 0.14);
--button-secondary-bg: var(--primitive-glass);
--button-secondary-text: var(--content-secondary);
--button-secondary-edge: var(--primitive-glass-edge);
--panel-bg: rgba(8, 13, 25, 0.72);
--panel-edge: var(--primitive-glass-edge);
--status-dot: var(--accent-light);
}
[data-theme='dawn'] {
--primitive-bg-a: #f5f7fc;
--primitive-bg-b: #eaeff8;
--primitive-bg-c: #dfe8f4;
--primitive-bg-d: #d3e0ef;
--primitive-star-core: #c9a24b;
--primitive-star-bright: #a96f2c;
--primitive-text-primary: rgba(22, 33, 58, 0.94);
--primitive-text-body: rgba(36, 49, 76, 0.86);
--primitive-text-aux: rgba(72, 88, 120, 0.62);
--primitive-text-faint: rgba(98, 112, 142, 0.48);
--primitive-nebula-a: rgba(122, 144, 220, 0.12);
--primitive-nebula-b: rgba(240, 196, 150, 0.12);
--primitive-glass: rgba(255, 255, 255, 0.34);
--primitive-glass-hover: rgba(255, 255, 255, 0.52);
--primitive-glass-edge: rgba(30, 50, 90, 0.12);
--primitive-glass-top: rgba(255, 255, 255, 0.65);
--primitive-mist-a: rgba(255, 255, 255, 0.55);
--primitive-mist-b: rgba(250, 214, 150, 0.3);
--primitive-cool-glow: rgba(96, 124, 205, 0.35);
--primitive-warm-glow: rgba(201, 162, 75, 0.5);
--primitive-shadow: rgba(42, 66, 105, 0.16);
--primitive-scene-opacity: 0.11;
--state-ready: #3f7058;
--state-waiting: #725b82;
--panel-bg: rgba(247, 250, 255, 0.76);
--button-primary-bg: rgba(45, 59, 84, 0.92);
--button-primary-text: #f8f4e9;
--button-primary-shadow: 0 12px 36px rgba(45, 59, 84, 0.16);
}
[data-theme='nebula'] {
--primitive-bg-a: #150e26;
--primitive-bg-b: #160f28;
--primitive-bg-c: #0e0919;
--primitive-bg-d: #080510;
--primitive-star-core: #ede4f6;
--primitive-star-bright: #f8f1ff;
--primitive-text-primary: rgba(238, 234, 248, 0.96);
--primitive-text-body: rgba(226, 220, 240, 0.88);
--primitive-text-aux: rgba(202, 194, 226, 0.62);
--primitive-text-faint: rgba(186, 178, 214, 0.42);
--primitive-nebula-a: rgba(138, 111, 201, 0.24);
--primitive-nebula-b: rgba(91, 111, 196, 0.11);
--primitive-glass: rgba(196, 178, 255, 0.05);
--primitive-glass-hover: rgba(196, 178, 255, 0.09);
--primitive-glass-edge: rgba(210, 198, 255, 0.14);
--primitive-glass-top: rgba(255, 255, 255, 0.07);
--primitive-mist-a: rgba(170, 150, 255, 0.12);
--primitive-mist-b: rgba(239, 217, 168, 0.05);
--primitive-cool-glow: rgba(170, 150, 255, 0.45);
--primitive-warm-glow: rgba(239, 217, 168, 0.5);
--primitive-shadow: rgba(0, 0, 0, 0.38);
--primitive-scene-opacity: 0.44;
--panel-bg: rgba(18, 12, 34, 0.76);
}
[data-theme='candle'] {
--primitive-bg-a: #1b130b;
--primitive-bg-b: #181108;
--primitive-bg-c: #100b05;
--primitive-bg-d: #0a0703;
--primitive-star-core: #ffd9a0;
--primitive-star-bright: #ffe7be;
--primitive-text-primary: rgba(246, 238, 226, 0.96);
--primitive-text-body: rgba(236, 226, 210, 0.88);
--primitive-text-aux: rgba(214, 198, 176, 0.62);
--primitive-text-faint: rgba(196, 180, 158, 0.42);
--primitive-nebula-a: rgba(214, 150, 80, 0.13);
--primitive-nebula-b: rgba(160, 110, 70, 0.07);
--primitive-glass: rgba(255, 214, 160, 0.045);
--primitive-glass-hover: rgba(255, 214, 160, 0.08);
--primitive-glass-edge: rgba(255, 220, 170, 0.13);
--primitive-glass-top: rgba(255, 255, 255, 0.06);
--primitive-mist-a: rgba(255, 196, 110, 0.09);
--primitive-mist-b: rgba(255, 214, 140, 0.04);
--primitive-cool-glow: rgba(214, 178, 140, 0.4);
--primitive-warm-glow: rgba(255, 196, 110, 0.55);
--primitive-shadow: rgba(0, 0, 0, 0.38);
--primitive-scene-opacity: 0.4;
--panel-bg: rgba(24, 17, 9, 0.78);
}
[data-theme='clear'] {
--primitive-bg-a: #f1f5f8;
--primitive-bg-b: #e8eef3;
--primitive-bg-c: #dee7ed;
--primitive-bg-d: #d2dee7;
--primitive-star-core: #5f7e9f;
--primitive-star-bright: #41628a;
--primitive-text-primary: rgba(20, 36, 52, 0.94);
--primitive-text-body: rgba(34, 50, 68, 0.86);
--primitive-text-aux: rgba(66, 86, 106, 0.62);
--primitive-text-faint: rgba(92, 110, 128, 0.48);
--primitive-nebula-a: rgba(100, 140, 180, 0.1);
--primitive-nebula-b: rgba(140, 170, 200, 0.08);
--primitive-glass: rgba(255, 255, 255, 0.32);
--primitive-glass-hover: rgba(255, 255, 255, 0.5);
--primitive-glass-edge: rgba(20, 50, 80, 0.12);
--primitive-glass-top: rgba(255, 255, 255, 0.65);
--primitive-mist-a: rgba(255, 255, 255, 0.55);
--primitive-mist-b: rgba(190, 214, 234, 0.35);
--primitive-cool-glow: rgba(78, 112, 150, 0.35);
--primitive-warm-glow: rgba(170, 138, 70, 0.5);
--primitive-shadow: rgba(37, 66, 90, 0.14);
--primitive-scene-opacity: 0.08;
--state-ready: #3f7058;
--state-waiting: #6a5b7a;
--panel-bg: rgba(246, 250, 253, 0.76);
--button-primary-bg: rgba(45, 66, 85, 0.92);
--button-primary-text: #f6f2e9;
--button-primary-shadow: 0 12px 36px rgba(45, 66, 85, 0.15);
}

View file

@ -1,24 +1,256 @@
import { StrictMode } from 'react' import { StrictMode, useCallback, useEffect, useMemo, useState } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { invoke } from '@tauri-apps/api/core'
import './design-tokens.css'
import './styles.css' import './styles.css'
function FoundationScreen() { type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type Panel = 'channel' | 'receipts' | 'settings' | null
interface HomeStatus {
schema: string
directLocalBrokerState: string
directConnectionCount: number
resumableSessionCount: number
codeRepositoryMountCount: number
pnccReceiptCount: number
updateState: string
automaticUpstreamUpdates: boolean
mcpRole: string
}
interface DiscoveryTicketReceipt {
schema: string
state: string
accountKey: string
laneId: string
clientInstanceId: string
discoveryTicket: string
issuedAtUnixMs: number
receiptId: string
}
interface ReceiptEvent {
sequence: number
kind: string
observedAtUnixMs: number
eventHash: string
}
interface ReceiptProjection {
events: ReceiptEvent[]
returnedEventCount: number
lastSequence: number
}
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' },
{ id: 'dawn', name: '晨湖曦光' },
{ id: 'nebula', name: '星云紫夜' },
{ id: 'candle', name: '烛畔暖湖' },
{ id: 'clear', name: '清浅澄湖' },
]
const previewStatus: HomeStatus = {
schema: 'hololake.home-status/preview',
directLocalBrokerState: 'PREVIEW',
directConnectionCount: 0,
resumableSessionCount: 0,
codeRepositoryMountCount: 0,
pnccReceiptCount: 0,
updateState: 'UNPROVISIONED_FAIL_CLOSED',
automaticUpstreamUpdates: false,
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
}
function getOrCreateLocalId(key: string, prefix: string) {
const existing = window.localStorage.getItem(key)
if (existing) return existing
const generated = `${prefix}-${crypto.randomUUID()}`
window.localStorage.setItem(key, generated)
return generated
}
function Icon({ name }: { name: 'settings' | 'arrow' | 'receipt' | 'close' | 'copy' }) {
const paths = {
settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-1.9 1.9-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.04 1.56V20h-2.7v-.08a1.7 1.7 0 0 0-1.05-1.56 1.7 1.7 0 0 0-1.88.34l-.06.06-1.9-1.9.06-.06A1.7 1.7 0 0 0 7.72 15a1.7 1.7 0 0 0-1.56-1.04H6v-2.7h.08A1.7 1.7 0 0 0 7.64 10a1.7 1.7 0 0 0-.34-1.88l-.06-.06 1.9-1.9.06.06a1.7 1.7 0 0 0 1.88.34A1.7 1.7 0 0 0 12.12 5V5h2.7v.08a1.7 1.7 0 0 0 1.04 1.56 1.7 1.7 0 0 0 1.88-.34l.06-.06 1.9 1.9-.06.06a1.7 1.7 0 0 0-.34 1.88 1.7 1.7 0 0 0 1.56 1.04H21v2.7h-.08A1.7 1.7 0 0 0 19.4 15Z"/></>,
arrow: <><path d="M5 12h13"/><path d="m14 7 5 5-5 5"/></>,
receipt: <><path d="M7 3h10v18l-2.5-1.5L12 21l-2.5-1.5L7 21Z"/><path d="M10 8h4M10 12h4"/></>,
close: <><path d="m7 7 10 10M17 7 7 17"/></>,
copy: <><rect x="8" y="8" width="10" height="11" rx="2"/><path d="M15 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2"/></>,
}
return <svg viewBox="0 0 24 24" aria-hidden="true">{paths[name]}</svg>
}
function HoloLakeApp() {
const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
const [status, setStatus] = useState<HomeStatus>(previewStatus)
const [panel, setPanel] = useState<Panel>(null)
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
const [receipts, setReceipts] = useState<ReceiptEvent[]>([])
const [message, setMessage] = useState('')
const refreshStatus = useCallback(async () => {
try {
setStatus(await invoke<HomeStatus>('get_hololake_home_status'))
} catch {
setStatus(previewStatus)
}
}, [])
useEffect(() => {
void refreshStatus()
const timer = window.setInterval(() => void refreshStatus(), 3000)
return () => window.clearInterval(timer)
}, [refreshStatus])
useEffect(() => {
document.documentElement.dataset.theme = theme
window.localStorage.setItem('hololake-theme', theme)
}, [theme])
const connectionState = useMemo(() => {
if (status.directConnectionCount > 0) return { label: '已连接', detail: `${status.directConnectionCount} 个直连通道在线`, tone: 'ready' }
if (status.resumableSessionCount > 0) return { label: '可续接', detail: `${status.resumableSessionCount} 个会话等待续接`, tone: 'waiting' }
return { label: '等待连接', detail: '本机直连核心已就绪', tone: 'quiet' }
}, [status])
const openReceipts = async () => {
setPanel('receipts')
try {
const projection = await invoke<ReceiptProjection>('query_pncc_receipt_projection', { input: { afterSequence: 0, limit: 25 } })
setReceipts(projection.events)
} catch {
setReceipts([])
}
}
const issueInvitation = async () => {
setMessage('')
try {
const receipt = await invoke<DiscoveryTicketReceipt>('issue_direct_local_discovery_ticket', {
input: {
accountId: getOrCreateLocalId('hololake-local-account', 'human-local'),
laneId: 'personal-channel',
clientInstanceId: getOrCreateLocalId('hololake-external-ai-client', 'external-ai'),
},
})
setTicket(receipt)
setMessage('连接邀请已生成。它只在这台电脑上有效。')
} catch (error) {
setMessage(`暂时无法生成连接邀请:${String(error)}`)
}
}
const invitationText = ticket ? JSON.stringify({
schema: 'hololake.direct-local-invitation/v1',
transport: 'INSTALLED_HOLOLAKE_EXECUTABLE_WITH_CONNECTOR_FLAG',
connectorArgument: '--connector',
openSession: {
operation: 'OPEN_SESSION',
input: {
accountId: window.localStorage.getItem('hololake-local-account'),
laneId: ticket.laneId,
clientInstanceId: ticket.clientInstanceId,
discoveryTicket: ticket.discoveryTicket,
},
},
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
}, null, 2) : ''
const copyInvitation = async () => {
try {
await navigator.clipboard.writeText(invitationText)
setMessage('已复制。把它交给桌面上的编程 AI 即可。')
} catch {
setMessage('复制没有成功,请选中下方邀请内容后复制。')
}
}
return ( return (
<main> <div className="world-shell">
<p className="eyebrow">HoloLake Native Desktop</p> <div className="scene-image" aria-hidden="true" />
<h1>线</h1> <div className="nebula nebula-one" aria-hidden="true" />
<p> <div className="nebula nebula-two" aria-hidden="true" />
Tauri <div className="mist" aria-hidden="true" />
</p> <div className="stars" aria-hidden="true">{Array.from({ length: 18 }, (_, index) => <i key={index} style={{ '--star-index': index } as React.CSSProperties} />)}</div>
<dl> <div className="glimmers" aria-hidden="true">{Array.from({ length: 11 }, (_, index) => <i key={index} style={{ '--glimmer-index': index } as React.CSSProperties} />)}</div>
<div><dt></dt><dd></dd></div>
<div><dt>广</dt><dd></dd></div> <header className="world-header">
<div><dt></dt><dd></dd></div> <div className="world-mark">
</dl> <span className="world-name"></span>
</main> <span className="world-route"></span>
</div>
<button className="icon-button" type="button" aria-label="打开设置" onClick={() => setPanel('settings')}><Icon name="settings" /></button>
</header>
<main className="home-content">
<section className="hero" aria-labelledby="home-title">
<p className="route-kicker">HOLOLAKE · PERSONAL CHANNEL</p>
<h1 id="home-title"><br /></h1>
<p className="hero-copy"> AI HoloLake 线</p>
<div className="hero-actions">
<button className="primary-action" type="button" onClick={() => setPanel('channel')}>
{status.directConnectionCount > 0 ? '已连接 · 查看通道' : '连接编程 AI'}
<Icon name="arrow" />
</button>
<button className="secondary-action" type="button" onClick={() => void openReceipts()}><Icon name="receipt" /></button>
</div>
</section>
<section className="status-cluster" aria-label="光湖运行状态">
<article className="status-item">
<span className={`status-light ${connectionState.tone}`} aria-hidden="true" />
<div><h2></h2><p>{connectionState.detail}</p></div>
</article>
<article className="status-item">
<span className={`status-light ${status.codeRepositoryMountCount > 0 ? 'ready' : 'quiet'}`} aria-hidden="true" />
<div><h2></h2><p>{status.codeRepositoryMountCount > 0 ? `已验证 · ${status.codeRepositoryMountCount} 个仓库` : '等待人类确认绑定'}</p></div>
</article>
<article className="status-item">
<span className={`status-light ${status.updateState.startsWith('READY') ? 'ready' : 'quiet'}`} aria-hidden="true" />
<div><h2>广</h2><p>{status.updateState.startsWith('READY') ? '仅接收光湖签名广播' : '信任根未配置 · 已关闭联网'}</p></div>
</article>
</section>
</main>
<footer className="world-footer">
<p><b>GH-AIOS</b><span></span></p>
<p className="trust-note"><i /> · </p>
</footer>
{panel && <div className="panel-backdrop" role="presentation" onMouseDown={(event) => event.target === event.currentTarget && setPanel(null)}>
<section className="detail-panel" role="dialog" aria-modal="true" aria-labelledby="panel-title">
<button className="panel-close" type="button" aria-label="关闭" onClick={() => setPanel(null)}><Icon name="close" /></button>
{panel === 'channel' && <>
<p className="panel-kicker"></p>
<h2 id="panel-title"> AI </h2>
<p className="panel-intro">MCP AI 使 HoloLake </p>
<div className="connection-summary"><span className={`status-light ${connectionState.tone}`} /><div><b>{connectionState.label}</b><span>{connectionState.detail}</span></div></div>
{ticket ? <>
<button className="primary-action panel-action" type="button" onClick={() => void copyInvitation()}><Icon name="copy" /> AI</button>
<pre className="invitation-preview">{invitationText}</pre>
</> : <button className="primary-action panel-action" type="button" onClick={() => void issueInvitation()}><Icon name="arrow" /></button>}
{message && <p className="panel-message" aria-live="polite">{message}</p>}
<p className="boundary-note"></p>
</>}
{panel === 'receipts' && <>
<p className="panel-kicker"></p>
<h2 id="panel-title"></h2>
<p className="panel-intro">线</p>
{receipts.length ? <ol className="receipt-list">{receipts.map((receipt) => <li key={receipt.sequence}><span>{receipt.sequence.toString().padStart(2, '0')}</span><div><b>{receipt.kind === 'REPOSITORY_BINDING_REVALIDATED' ? '仓库绑定已复核' : '远端对象读取已验证'}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></li>)}</ol> : <div className="empty-state"><i /><b></b><span></span></div>}
</>}
{panel === 'settings' && <>
<p className="panel-kicker"></p>
<h2 id="panel-title"></h2>
<p className="panel-intro"></p>
<div className="theme-list">{themes.map((choice) => <button key={choice.id} type="button" className={theme === choice.id ? 'selected' : ''} onClick={() => setTheme(choice.id)}><i className={`theme-swatch ${choice.id}`} /><span><b>{choice.name}</b><small>{choice.id === 'night' ? '当前定版基色' : '五色湖定版主题'}</small></span><em>{theme === choice.id ? '已选择' : ''}</em></button>)}</div>
<p className="boundary-note"> HoloLake 广</p>
</>}
</section>
</div>}
</div>
) )
} }
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(<StrictMode><HoloLakeApp /></StrictMode>)
<StrictMode><FoundationScreen /></StrictMode>,
)

View file

@ -1,16 +1,138 @@
* { box-sizing: border-box; }
:root { :root {
font-family: ui-sans-serif, system-ui, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei UI", sans-serif;
color: #eaf0ff; color: var(--content-primary);
background: #07101f; background: var(--surface-depth);
font-synthesis: none; font-synthesis: none;
text-rendering: geometricPrecision;
} }
body { margin: 0; min-width: 320px; min-height: 100vh; } body { margin: 0; min-width: 320px; min-height: 100vh; overflow: hidden; }
main { max-width: 720px; margin: 0 auto; padding: 12vh 32px; } button { font: inherit; }
.eyebrow { color: #82a7ff; letter-spacing: .08em; text-transform: uppercase; } button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }
h1 { font-size: clamp(2rem, 5vw, 4rem); line-height: 1.05; }
p { color: #b7c5e5; line-height: 1.7; } .world-shell {
dl { margin-top: 48px; border-top: 1px solid #263554; } position: relative;
dl div { display: flex; justify-content: space-between; gap: 24px; padding: 16px 0; border-bottom: 1px solid #263554; } min-height: 100vh;
dt { color: #8fa4cf; } overflow: hidden;
dd { margin: 0; text-align: right; } isolation: isolate;
background: linear-gradient(180deg, var(--surface-sky) 0%, var(--surface-horizon) 40%, var(--surface-lake) 65%, var(--surface-depth) 100%);
transition: background 450ms ease, color 300ms ease;
}
.scene-image {
position: absolute;
z-index: -4;
inset: 25% 0 0;
background-image: linear-gradient(180deg, transparent 0%, var(--surface-depth) 100%), url('./assets/hololake-night-lake.png');
background-size: cover;
background-position: center 42%;
opacity: var(--primitive-scene-opacity);
mix-blend-mode: screen;
transition: opacity 450ms ease;
}
.nebula { position: absolute; z-index: -3; border-radius: 50%; filter: blur(42px); pointer-events: none; }
.nebula-one { width: 58vw; height: 38vw; left: -12vw; top: -18vw; background: radial-gradient(circle, var(--primitive-nebula-a), transparent 68%); }
.nebula-two { width: 48vw; height: 32vw; right: -12vw; top: 4vh; background: radial-gradient(circle, var(--primitive-nebula-b), transparent 70%); }
.mist { position: absolute; z-index: -2; left: 0; right: 0; top: 38%; height: 36%; background: radial-gradient(ellipse at 50% 52%, var(--primitive-mist-a), var(--primitive-mist-b) 45%, transparent 72%); filter: blur(24px); pointer-events: none; }
.stars i, .glimmers i { position: absolute; display: block; border-radius: 50%; pointer-events: none; }
.stars i { width: calc(1px + (var(--star-index) % 3) * 1px); height: calc(1px + (var(--star-index) % 3) * 1px); left: calc(4% + (var(--star-index) * 5.2%)); top: calc(5% + (var(--star-index) % 5) * 5.4%); background: var(--accent-light); opacity: calc(.16 + (var(--star-index) % 4) * .09); box-shadow: 0 0 8px var(--primitive-warm-glow); animation: breathe calc(4s + (var(--star-index) % 5) * .7s) ease-in-out infinite; }
.glimmers i { width: calc(2px + (var(--glimmer-index) % 3) * 2px); height: 2px; left: calc(9% + (var(--glimmer-index) * 8.1%)); top: calc(52% + (var(--glimmer-index) % 4) * 6.4%); background: var(--accent-light); box-shadow: 0 0 12px var(--primitive-cool-glow); opacity: .18; animation: glimmer calc(5s + (var(--glimmer-index) % 4) * 1.2s) ease-in-out infinite; }
@keyframes breathe { 50% { opacity: .75; transform: scale(1.5); } }
@keyframes glimmer { 50% { opacity: .7; transform: translateX(16px) scaleX(2.4); } }
.world-header { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 clamp(28px, 5vw, 70px); }
.world-mark { display: flex; align-items: baseline; gap: 17px; }
.world-name { font-size: 21px; font-weight: 610; letter-spacing: .18em; }
.world-route { color: var(--content-muted); font-size: 12px; letter-spacing: .16em; }
.icon-button, .panel-close { border: 0; color: var(--content-muted); background: transparent; display: grid; place-items: center; cursor: pointer; border-radius: 50%; transition: color 180ms ease, background 180ms ease; }
.icon-button { width: 42px; height: 42px; }
.icon-button:hover, .panel-close:hover { color: var(--accent-light); background: var(--primitive-glass-hover); }
.icon-button svg, .panel-close svg, .hero-actions svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; }
.home-content { min-height: calc(100vh - 156px); display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(285px, .45fr); align-items: center; gap: clamp(48px, 7vw, 112px); padding: 0 clamp(54px, 8vw, 126px) 28px; }
.hero { max-width: 680px; transform: translateY(-2vh); }
.route-kicker, .panel-kicker { margin: 0 0 21px; color: var(--accent-light); font-size: 11px; font-weight: 630; letter-spacing: .22em; }
.hero h1 { margin: 0; max-width: 700px; color: var(--content-primary); font-size: clamp(40px, 4.15vw, 58px); font-weight: 430; line-height: 1.22; letter-spacing: -.035em; text-wrap: balance; }
.hero-copy { max-width: 540px; margin: 27px 0 0; color: var(--content-muted); font-size: 14px; line-height: 1.9; letter-spacing: .035em; }
.hero-actions { display: flex; align-items: center; gap: 14px; margin-top: 37px; }
.primary-action, .secondary-action { min-height: 46px; border-radius: 999px; padding: 0 22px; display: inline-flex; align-items: center; justify-content: center; gap: 12px; cursor: pointer; transition: transform 180ms ease, background 180ms ease, box-shadow 180ms ease; }
.primary-action { border: 0; color: var(--button-primary-text); background: var(--button-primary-bg); box-shadow: var(--button-primary-shadow); font-weight: 620; }
.secondary-action { color: var(--button-secondary-text); border: 1px solid var(--button-secondary-edge); background: var(--button-secondary-bg); backdrop-filter: blur(18px); }
.primary-action:hover, .secondary-action:hover { transform: translateY(-2px); }
.secondary-action:hover { background: var(--primitive-glass-hover); }
.status-cluster { display: grid; gap: 30px; width: min(100%, 330px); justify-self: end; }
.status-item { display: grid; grid-template-columns: 10px 1fr; align-items: start; gap: 15px; padding: 5px 0; }
.status-item h2 { margin: 0 0 8px; color: var(--content-secondary); font-size: 13px; font-weight: 590; letter-spacing: .06em; }
.status-item p { margin: 0; color: var(--content-muted); opacity: .72; font-size: 11.5px; line-height: 1.5; letter-spacing: .04em; }
.status-light { width: 6px; height: 6px; margin-top: 5px; flex: 0 0 auto; border-radius: 50%; background: var(--state-quiet); box-shadow: 0 0 14px currentColor; }
.status-light.ready { color: var(--state-ready); background: currentColor; }
.status-light.waiting { color: var(--state-waiting); background: currentColor; }
.status-light.quiet { color: var(--state-quiet); background: currentColor; }
.world-footer { height: 78px; display: flex; align-items: center; justify-content: space-between; padding: 0 clamp(28px, 5vw, 70px); color: var(--content-faint); font-size: 10.5px; letter-spacing: .09em; }
.world-footer p { margin: 0; display: flex; align-items: center; gap: 13px; }
.world-footer b { color: var(--accent-light); font-size: 11.5px; letter-spacing: .14em; }
.trust-note i { width: 4px; height: 4px; border-radius: 50%; background: var(--accent-light); opacity: .65; }
.panel-backdrop { position: fixed; z-index: 20; inset: 0; display: flex; align-items: stretch; justify-content: flex-end; background: rgba(2, 4, 9, .28); backdrop-filter: blur(5px); }
.detail-panel { position: relative; width: min(500px, 92vw); height: 100%; overflow-y: auto; padding: 80px 54px 50px; color: var(--content-primary); background: var(--panel-bg); box-shadow: -26px 0 80px var(--primitive-shadow); backdrop-filter: blur(38px) saturate(115%); animation: panel-in 240ms ease-out; }
@keyframes panel-in { from { transform: translateX(24px); opacity: 0; } }
.panel-close { position: absolute; right: 34px; top: 30px; width: 38px; height: 38px; }
.detail-panel h2 { margin: 0; max-width: 380px; font-size: 31px; font-weight: 480; line-height: 1.28; letter-spacing: -.025em; }
.panel-intro { margin: 19px 0 30px; color: var(--content-muted); font-size: 13px; line-height: 1.85; }
.connection-summary { display: flex; align-items: flex-start; gap: 15px; margin: 32px 0; padding: 20px 22px; border-radius: 20px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
.connection-summary div { display: grid; gap: 6px; }
.connection-summary b { color: var(--content-secondary); font-size: 13px; }
.connection-summary span { color: var(--content-faint); font-size: 11.5px; }
.panel-action { min-height: 46px; }
.panel-message { color: var(--content-muted); font-size: 11.5px; line-height: 1.6; }
.invitation-preview { max-height: 180px; overflow: auto; margin: 18px 0; padding: 17px; border-radius: 16px; color: var(--content-faint); background: rgba(0, 0, 0, .13); font: 10px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; word-break: break-all; }
.boundary-note { margin-top: 34px; color: var(--content-faint); font-size: 10.5px; line-height: 1.75; }
.receipt-list { list-style: none; display: grid; gap: 14px; margin: 30px 0 0; padding: 0; }
.receipt-list li { display: grid; grid-template-columns: 34px 1fr; gap: 14px; align-items: start; padding: 16px 18px; border-radius: 17px; background: var(--primitive-glass); }
.receipt-list li > span { color: var(--accent-light); font: 11px ui-monospace, monospace; }
.receipt-list li div { display: grid; gap: 7px; }
.receipt-list b { color: var(--content-secondary); font-size: 12px; font-weight: 580; }
.receipt-list small { color: var(--content-faint); font-size: 10px; }
.empty-state { min-height: 260px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.empty-state i { width: 7px; height: 7px; margin-bottom: 20px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 32px 12px var(--primitive-warm-glow); }
.empty-state b { color: var(--content-secondary); font-size: 13px; font-weight: 570; }
.empty-state span { max-width: 260px; margin-top: 10px; color: var(--content-faint); font-size: 11px; line-height: 1.7; }
.theme-list { display: grid; gap: 9px; margin-top: 30px; }
.theme-list button { width: 100%; min-height: 66px; display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: 14px; padding: 10px 14px; border: 0; border-radius: 17px; color: var(--content-secondary); background: transparent; text-align: left; cursor: pointer; }
.theme-list button:hover, .theme-list button.selected { background: var(--primitive-glass-hover); }
.theme-list span { display: grid; gap: 5px; }
.theme-list b { font-size: 12.5px; font-weight: 590; white-space: nowrap; }
.theme-list small, .theme-list em { color: var(--content-faint); font-size: 10px; font-style: normal; }
.theme-list button.selected em { color: var(--accent-light); }
.theme-swatch { width: 34px; height: 34px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(255,255,255,.18), 0 6px 18px var(--primitive-shadow); }
.theme-swatch.night { background: linear-gradient(145deg, #0a1020, #04070d 70%); }
.theme-swatch.dawn { background: linear-gradient(145deg, #f5f7fc, #d3e0ef 70%); }
.theme-swatch.nebula { background: linear-gradient(145deg, #281b47, #080510 70%); }
.theme-swatch.candle { background: linear-gradient(145deg, #4a2d13, #0a0703 70%); }
.theme-swatch.clear { background: linear-gradient(145deg, #f1f5f8, #b9ccda 70%); }
@media (max-width: 900px) {
.home-content { grid-template-columns: 1fr; align-content: center; gap: 50px; padding-inline: 62px; }
.hero { transform: none; }
.status-cluster { width: 100%; grid-template-columns: repeat(3, 1fr); justify-self: stretch; gap: 26px; }
}
@media (max-height: 640px) {
.world-header, .world-footer { height: 62px; }
.home-content { min-height: calc(100vh - 124px); }
.hero h1 { font-size: clamp(36px, 5vw, 56px); }
.hero-copy { margin-top: 18px; }
.hero-actions { margin-top: 25px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}