diff --git a/product-source/hololake-native-desktop/.gitignore b/product-source/hololake-native-desktop/.gitignore index 97ef180..e5861af 100644 --- a/product-source/hololake-native-desktop/.gitignore +++ b/product-source/hololake-native-desktop/.gitignore @@ -5,6 +5,7 @@ *.tsbuildinfo /vite.config.js /vite.config.d.ts +/work/ *.key *.p12 *.pfx diff --git a/product-source/hololake-native-desktop/audit/ghs-014-stage-one-ui-receipt.json b/product-source/hololake-native-desktop/audit/ghs-014-stage-one-ui-receipt.json new file mode 100644 index 0000000..a738848 --- /dev/null +++ b/product-source/hololake-native-desktop/audit/ghs-014-stage-one-ui-receipt.json @@ -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 + } +} diff --git a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md index d82306b..d74f107 100644 --- a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md +++ b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md @@ -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. +## 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 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. diff --git a/product-source/hololake-native-desktop/foundation.json b/product-source/hololake-native-desktop/foundation.json index b2e868f..0f05169 100644 --- a/product-source/hololake-native-desktop/foundation.json +++ b/product-source/hololake-native-desktop/foundation.json @@ -1,10 +1,15 @@ { "schema": "hololake.native-desktop-foundation/v1", "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", - "product_ui_implementation_started": false, - "selected_visual_direction_present": false, + "product_ui_implementation_started": true, + "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": [], "production_update_endpoint_owner": "HOLOLAKE_ONLY", "release_broadcast_contract": "contracts/release-broadcast.schema.json", @@ -37,7 +42,7 @@ "pncc_receipt_projection_implemented": true, "pncc_authenticated_direct_broker_integration_implemented": true, "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_execution_limb_implemented": false, "pncc_persona_lease_acquisition_implemented": false, diff --git a/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs b/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs index 37d7c64..9c3a7d6 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/direct_local_broker.rs @@ -27,7 +27,7 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, mpsc, Arc, }; use std::thread; @@ -40,10 +40,33 @@ const MAX_REQUEST_BYTES: u64 = 1024 * 1024; pub struct DirectLocalBrokerHandle { shutdown: Arc, + authenticated_connections: Arc, socket_path: PathBuf, worker: Option>, } +impl DirectLocalBrokerHandle { + pub fn active_connection_count(&self) -> usize { + self.authenticated_connections.load(Ordering::Acquire) + } +} + +struct AuthenticatedConnectionGuard(Arc); + +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 { fn drop(&mut self) { self.shutdown.store(true, Ordering::Release); @@ -296,7 +319,9 @@ fn start_at( )?; let shutdown = Arc::new(AtomicBool::new(false)); + let authenticated_connections = Arc::new(AtomicUsize::new(0)); let worker_shutdown = Arc::clone(&shutdown); + let worker_authenticated_connections = Arc::clone(&authenticated_connections); let worker_socket = socket_path.clone(); let (ready_sender, ready_receiver) = mpsc::sync_channel(1); let worker = thread::Builder::new() @@ -305,12 +330,15 @@ fn start_at( let _ = ready_sender.send(()); serve( listener, - session_root, - routing_root, - pncc_mount_root, - pncc_remote_root, - pncc_projection_root, + BrokerStorageRoots { + session: session_root, + routing: routing_root, + pncc_mount: pncc_mount_root, + pncc_remote: pncc_remote_root, + pncc_projection: pncc_projection_root, + }, &worker_shutdown, + &worker_authenticated_connections, ); let _ = fs::remove_file(worker_socket); }) @@ -321,6 +349,7 @@ fn start_at( Ok(DirectLocalBrokerHandle { shutdown, + authenticated_connections, socket_path, worker: Some(worker), }) @@ -328,12 +357,9 @@ fn start_at( fn serve( listener: UnixListener, - session_root: PathBuf, - routing_root: PathBuf, - pncc_mount_root: PathBuf, - pncc_remote_root: PathBuf, - pncc_projection_root: PathBuf, + roots: BrokerStorageRoots, shutdown: &AtomicBool, + authenticated_connections: &Arc, ) { while !shutdown.load(Ordering::Acquire) { match listener.accept() { @@ -341,11 +367,15 @@ fn serve( if shutdown.load(Ordering::Acquire) { break; } - let root = session_root.clone(); - let routes = routing_root.clone(); - let pncc_mounts = pncc_mount_root.clone(); - let pncc_remote = pncc_remote_root.clone(); - let pncc_projection = pncc_projection_root.clone(); + if stream.set_nonblocking(false).is_err() { + continue; + } + let root = roots.session.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() .name("hololake-direct-local-client".into()) .spawn(move || { @@ -356,6 +386,7 @@ fn serve( &pncc_mounts, &pncc_remote, &pncc_projection, + client_authenticated_connections, ) }); } @@ -374,12 +405,14 @@ fn serve_connection( pncc_mount_root: &Path, pncc_remote_root: &Path, pncc_projection_root: &Path, + authenticated_connections: Arc, ) { let read_stream = match stream.try_clone() { Ok(stream) => stream, Err(_) => return, }; let mut reader = BufReader::new(read_stream); + let mut authenticated_connection = None; loop { let mut bytes = Vec::new(); let read = match reader @@ -393,6 +426,7 @@ fn serve_connection( if read == 0 { return; } + let request_authenticates = request_establishes_authenticated_connection(&bytes); let response = if read as u64 > MAX_REQUEST_BYTES || !bytes.ends_with(b"\n") { BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE") } else { @@ -405,6 +439,12 @@ fn serve_connection( &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() || stream.write_all(b"\n").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::(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( session_root: &Path, routing_root: &Path, @@ -840,4 +892,68 @@ mod tests { assert_eq!(verified["result"]["emptyMeansOffline"], false); 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::(&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); + } } diff --git a/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs b/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs index 69b8971..1cc2ec0 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/direct_local_session.rs @@ -203,6 +203,43 @@ pub(crate) fn direct_session_root(app: &AppHandle) -> Result { .map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}")) } +pub(crate) fn active_session_count_at(root: &Path) -> Result { + 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( root: &Path, input: OpenSessionInput, diff --git a/product-source/hololake-native-desktop/src-tauri/src/home_status.rs b/product-source/hololake-native-desktop/src-tauri/src/home_status.rs new file mode 100644 index 0000000..f2c946c --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/home_status.rs @@ -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 { + 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", + }) +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/lib.rs b/product-source/hololake-native-desktop/src-tauri/src/lib.rs index 029e376..c4e2eda 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ mod direct_local_broker; mod direct_local_session; mod dynamic_capability_routing; +mod home_status; mod local_development_bridge; mod pncc_receipt_projection; mod pncc_remote_git; @@ -17,6 +18,7 @@ pub fn run_connector() -> Result<(), String> { pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ + home_status::get_hololake_home_status, direct_local_session::issue_direct_local_discovery_ticket, direct_local_session::open_direct_local_session, direct_local_session::resume_direct_local_session, diff --git a/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs b/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs index 229d3d2..1972d43 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/pncc_receipt_projection.rs @@ -102,6 +102,11 @@ pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result { .map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}")) } +pub(crate) fn projection_event_count_at(root: &Path) -> Result { + let _lock = lock_projection(root)?; + Ok(load_and_verify(root)?.events.len()) +} + pub(crate) fn append_repository_binding_at( root: &Path, receipt: &PnccRepositoryMountReceipt, diff --git a/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs b/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs index 4e4cc12..5a15102 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/pncc_repository_binding.rs @@ -77,6 +77,32 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result Result { + 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( root: &Path, input: InspectMountedPnccRepositoryInput, diff --git a/product-source/hololake-native-desktop/src-tauri/src/release_trust.rs b/product-source/hololake-native-desktop/src-tauri/src/release_trust.rs index c7da0a9..3a402b5 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/release_trust.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/release_trust.rs @@ -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 { let trust: ReleaseTrust = serde_json::from_str(raw) .map_err(|error| format!("invalid HoloLake release trust document: {error}"))?; diff --git a/product-source/hololake-native-desktop/src/assets/hololake-night-lake.png b/product-source/hololake-native-desktop/src/assets/hololake-night-lake.png new file mode 100644 index 0000000..8ec213b Binary files /dev/null and b/product-source/hololake-native-desktop/src/assets/hololake-night-lake.png differ diff --git a/product-source/hololake-native-desktop/src/design-tokens.css b/product-source/hololake-native-desktop/src/design-tokens.css new file mode 100644 index 0000000..6f1c6dd --- /dev/null +++ b/product-source/hololake-native-desktop/src/design-tokens.css @@ -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); +} diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 99216f1..ad47a1d 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -1,24 +1,256 @@ -import { StrictMode } from 'react' +import { StrictMode, useCallback, useEffect, useMemo, useState } from 'react' import { createRoot } from 'react-dom/client' +import { invoke } from '@tauri-apps/api/core' +import './design-tokens.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: <>, + arrow: <>, + receipt: <>, + close: <>, + copy: <>, + } + return +} + +function HoloLakeApp() { + const [theme, setTheme] = useState(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night') + const [status, setStatus] = useState(previewStatus) + const [panel, setPanel] = useState(null) + const [ticket, setTicket] = useState(null) + const [receipts, setReceipts] = useState([]) + const [message, setMessage] = useState('') + + const refreshStatus = useCallback(async () => { + try { + setStatus(await invoke('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('query_pncc_receipt_projection', { input: { afterSequence: 0, limit: 25 } }) + setReceipts(projection.events) + } catch { + setReceipts([]) + } + } + + const issueInvitation = async () => { + setMessage('') + try { + const receipt = await invoke('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 ( -
-

HoloLake Native Desktop

-

新的光湖桌面主线正在建立信任根

-

- 当前只完成最小 Tauri 外壳与更新安全边界。知识工作区将在双供体审计和视觉方向确定后开始。 -

-
-
上游更新
已关闭
-
光湖发布广播
等待正式端点与公钥
-
下载与安装
必须由人类选择
-
-
+
+