@@ -357,6 +357,14 @@ struct WorkspaceMountOnboardingReport {
357357 launch_strategy: String,
358358}
359359
360+ #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
361+ #[serde(rename_all = "camelCase")]
362+ struct FileProviderEnablementReport {
363+ state: String,
364+ message: String,
365+ path: Option<String>,
366+ }
367+
360368#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361369enum MacosWorkspaceMountOnboardingState {
362370 Created,
@@ -369,7 +377,7 @@ impl MacosWorkspaceMountOnboardingState {
369377 fn as_str(self) -> &'static str {
370378 match self {
371379 Self::Created => "created",
372- Self::ApprovalRequired => "approval_required ",
380+ Self::ApprovalRequired => "needs_finder_enable ",
373381 Self::WaitingForCloudStorageRoot => "waiting_for_cloudstorage_root",
374382 Self::Failed => "failed",
375383 }
@@ -1929,6 +1937,33 @@ async fn run_workspace_mount_onboarding(
19291937 report
19301938}
19311939
1940+ #[tauri::command]
1941+ async fn file_provider_enablement_status() -> FileProviderEnablementReport {
1942+ tauri::async_runtime::spawn_blocking(macos_file_provider_enablement_status_blocking)
1943+ .await
1944+ .unwrap_or_else(|error| FileProviderEnablementReport {
1945+ state: "unavailable".to_string(),
1946+ message: format!("File Provider status worker failed: {error}"),
1947+ path: None,
1948+ })
1949+ }
1950+
1951+ #[tauri::command]
1952+ async fn reveal_file_provider_enablement() -> ActionReport {
1953+ tauri::async_runtime::spawn_blocking(reveal_file_provider_enablement_blocking)
1954+ .await
1955+ .map_or_else(
1956+ |error| ActionReport {
1957+ ok: false,
1958+ message: format!("Finder worker failed: {error}"),
1959+ },
1960+ |result| match result {
1961+ Ok(message) => ActionReport { ok: true, message },
1962+ Err(message) => ActionReport { ok: false, message },
1963+ },
1964+ )
1965+ }
1966+
19321967async fn create_desktop_mount_command(
19331968 app: AppHandle,
19341969 request: CreateDesktopMountRequest,
@@ -10101,7 +10136,7 @@ fn workspace_mount_onboarding_curated_message(
1010110136) -> Option<&'static str> {
1010210137 match state {
1010310138 MacosWorkspaceMountOnboardingState::ApprovalRequired => {
10104- Some("Enable Locality in Finder, then return here and click Check again .")
10139+ Some("In Finder, click Enable for Locality. Locality will continue automatically .")
1010510140 }
1010610141 MacosWorkspaceMountOnboardingState::WaitingForCloudStorageRoot => {
1010710142 Some("Locality is still waiting for the CloudStorage folder to appear.")
@@ -10193,6 +10228,142 @@ fn macos_workspace_mount_domain_user_enabled() -> Result<bool, String> {
1019310228 .unwrap_or(false))
1019410229}
1019510230
10231+ fn classify_macos_file_provider_enablement(
10232+ user_enabled: Option<bool>,
10233+ fallback_path: Option<PathBuf>,
10234+ resolved_root: Result<Option<PathBuf>, String>,
10235+ ) -> FileProviderEnablementReport {
10236+ let path = |value: Option<PathBuf>| value.map(|path| path.display().to_string());
10237+ match user_enabled {
10238+ None => FileProviderEnablementReport {
10239+ state: "not_registered".to_string(),
10240+ message: "Locality is preparing the Finder location.".to_string(),
10241+ path: path(fallback_path),
10242+ },
10243+ Some(false) => FileProviderEnablementReport {
10244+ state: "needs_finder_enable".to_string(),
10245+ message: "In Finder, click Enable for Locality.".to_string(),
10246+ path: path(fallback_path),
10247+ },
10248+ Some(true) => match resolved_root {
10249+ Ok(Some(root)) => FileProviderEnablementReport {
10250+ state: "ready".to_string(),
10251+ message: "Locality is enabled in Finder.".to_string(),
10252+ path: path(Some(root)),
10253+ },
10254+ Ok(None) => FileProviderEnablementReport {
10255+ state: "waiting_for_root".to_string(),
10256+ message: "Finishing the Locality folder setup.".to_string(),
10257+ path: path(fallback_path),
10258+ },
10259+ Err(message) => FileProviderEnablementReport {
10260+ state: "unavailable".to_string(),
10261+ message,
10262+ path: path(fallback_path),
10263+ },
10264+ },
10265+ }
10266+ }
10267+
10268+ fn macos_file_provider_domain_status(
10269+ report: &serde_json::Value,
10270+ ) -> (Option<bool>, Option<PathBuf>) {
10271+ let domain = report
10272+ .get("domains")
10273+ .and_then(serde_json::Value::as_array)
10274+ .and_then(|domains| {
10275+ domains.iter().find(|domain| {
10276+ domain.get("identifier").and_then(serde_json::Value::as_str)
10277+ == Some(localityd::file_provider::MACOS_FILE_PROVIDER_DOMAIN_ID)
10278+ })
10279+ });
10280+ let user_enabled = domain
10281+ .and_then(|domain| domain.get("userEnabled"))
10282+ .and_then(serde_json::Value::as_bool);
10283+ let path = domain
10284+ .and_then(|domain| domain.get("url"))
10285+ .and_then(serde_json::Value::as_str)
10286+ .filter(|url| !url.is_empty())
10287+ .map(PathBuf::from);
10288+ (user_enabled, path)
10289+ }
10290+
10291+ #[cfg(target_os = "macos")]
10292+ fn macos_file_provider_enablement_status_blocking() -> FileProviderEnablementReport {
10293+ let provider_roots = macos_file_provider_cloud_storage_roots();
10294+ let report = match run_macos_file_provider_helper("list", Vec::new()) {
10295+ Ok(report) => report,
10296+ Err(error) => {
10297+ return FileProviderEnablementReport {
10298+ state: "unavailable".to_string(),
10299+ message: error.message(),
10300+ path: provider_roots
10301+ .first()
10302+ .map(|path| path.display().to_string()),
10303+ };
10304+ }
10305+ };
10306+ let (user_enabled, reported_path) = macos_file_provider_domain_status(&report.helper_report);
10307+ let fallback_path = reported_path.or_else(|| provider_roots.first().cloned());
10308+
10309+ let resolved_root = if user_enabled == Some(true) {
10310+ match macos_file_provider_domain_url(
10311+ localityd::file_provider::MACOS_FILE_PROVIDER_DOMAIN_ID,
10312+ ) {
10313+ Ok(root) => Ok(Some(root)),
10314+ Err(error) if recoverable_macos_file_provider_activation_error(&error.message()) => {
10315+ Ok(None)
10316+ }
10317+ Err(error) => Err(error.message()),
10318+ }
10319+ } else {
10320+ Ok(None)
10321+ };
10322+ classify_macos_file_provider_enablement(user_enabled, fallback_path, resolved_root)
10323+ }
10324+
10325+ #[cfg(not(target_os = "macos"))]
10326+ fn macos_file_provider_enablement_status_blocking() -> FileProviderEnablementReport {
10327+ FileProviderEnablementReport {
10328+ state: "unavailable".to_string(),
10329+ message: "File Provider enablement is only available on macOS.".to_string(),
10330+ path: None,
10331+ }
10332+ }
10333+
10334+ fn macos_file_provider_reveal_path(
10335+ domain_path: Option<&Path>,
10336+ candidates: &[PathBuf],
10337+ ) -> Option<PathBuf> {
10338+ domain_path
10339+ .into_iter()
10340+ .chain(candidates.iter().map(PathBuf::as_path))
10341+ .find_map(|candidate| {
10342+ candidate
10343+ .ancestors()
10344+ .find(|ancestor| ancestor.is_dir())
10345+ .map(Path::to_path_buf)
10346+ })
10347+ }
10348+
10349+ #[cfg(target_os = "macos")]
10350+ fn reveal_file_provider_enablement_blocking() -> Result<String, String> {
10351+ let report = macos_file_provider_enablement_status_blocking();
10352+ let domain_path = report.path.as_deref().map(Path::new);
10353+ let path =
10354+ macos_file_provider_reveal_path(domain_path, &macos_file_provider_cloud_storage_roots())
10355+ .ok_or_else(|| {
10356+ "Could not find the Locality location or its CloudStorage parent.".to_string()
10357+ })?;
10358+ open_in_file_manager(&path)?;
10359+ Ok(format!("Opened {} in Finder.", path.display()))
10360+ }
10361+
10362+ #[cfg(not(target_os = "macos"))]
10363+ fn reveal_file_provider_enablement_blocking() -> Result<String, String> {
10364+ Err("File Provider enablement is only available on macOS.".to_string())
10365+ }
10366+
1019610367#[cfg(not(target_os = "macos"))]
1019710368fn macos_workspace_mount_domain_user_enabled() -> Result<bool, String> {
1019810369 Ok(false)
@@ -13642,6 +13813,100 @@ mod tests {
1364213813 ));
1364313814 }
1364413815
13816+ #[test]
13817+ fn file_provider_enablement_report_distinguishes_registration_and_approval() {
13818+ let missing = super::classify_macos_file_provider_enablement(None, None, Ok(None));
13819+ assert_eq!(missing.state, "not_registered");
13820+
13821+ let disabled = super::classify_macos_file_provider_enablement(
13822+ Some(false),
13823+ Some(PathBuf::from("/Users/test/Library/CloudStorage/Locality")),
13824+ Ok(None),
13825+ );
13826+ assert_eq!(disabled.state, "needs_finder_enable");
13827+ assert_eq!(
13828+ disabled.path.as_deref(),
13829+ Some("/Users/test/Library/CloudStorage/Locality")
13830+ );
13831+ }
13832+
13833+ #[test]
13834+ fn file_provider_domain_status_reads_helper_url_while_disabled() {
13835+ let helper_report = serde_json::json!({
13836+ "domains": [{
13837+ "identifier": "loc",
13838+ "userEnabled": false,
13839+ "url": "/Users/test/Library/CloudStorage/LocalityPromptTest"
13840+ }]
13841+ });
13842+
13843+ let (user_enabled, path) = super::macos_file_provider_domain_status(&helper_report);
13844+
13845+ assert_eq!(user_enabled, Some(false));
13846+ assert_eq!(
13847+ path,
13848+ Some(PathBuf::from(
13849+ "/Users/test/Library/CloudStorage/LocalityPromptTest"
13850+ ))
13851+ );
13852+ }
13853+
13854+ #[test]
13855+ fn file_provider_enablement_report_waits_for_enabled_root() {
13856+ let report = super::classify_macos_file_provider_enablement(
13857+ Some(true),
13858+ Some(PathBuf::from("/Users/test/Library/CloudStorage/Locality")),
13859+ Ok(None),
13860+ );
13861+
13862+ assert_eq!(report.state, "waiting_for_root");
13863+ assert!(report.message.contains("Finishing"));
13864+ }
13865+
13866+ #[test]
13867+ fn file_provider_enablement_report_is_ready_only_with_resolved_root() {
13868+ let root = PathBuf::from("/Users/test/Library/CloudStorage/Locality");
13869+ let report = super::classify_macos_file_provider_enablement(
13870+ Some(true),
13871+ Some(root.clone()),
13872+ Ok(Some(root.clone())),
13873+ );
13874+
13875+ assert_eq!(report.state, "ready");
13876+ assert_eq!(report.path.as_deref(), root.to_str());
13877+ }
13878+
13879+ #[test]
13880+ fn file_provider_enablement_report_exposes_unavailable_errors() {
13881+ let report = super::classify_macos_file_provider_enablement(
13882+ Some(true),
13883+ None,
13884+ Err("File Provider helper unavailable".to_string()),
13885+ );
13886+
13887+ assert_eq!(report.state, "unavailable");
13888+ assert_eq!(report.message, "File Provider helper unavailable");
13889+ }
13890+
13891+ #[test]
13892+ fn file_provider_reveal_path_prefers_domain_then_existing_parent() {
13893+ let temp = TestTempDir::new("file-provider-reveal");
13894+ let cloud_storage = temp.path().join("Library/CloudStorage");
13895+ fs::create_dir_all(&cloud_storage).expect("create CloudStorage parent");
13896+ let domain = cloud_storage.join("Locality");
13897+
13898+ assert_eq!(
13899+ super::macos_file_provider_reveal_path(Some(&domain), &[domain.clone()]),
13900+ Some(cloud_storage)
13901+ );
13902+
13903+ fs::create_dir_all(&domain).expect("create domain root");
13904+ assert_eq!(
13905+ super::macos_file_provider_reveal_path(Some(&domain), &[domain.clone()]),
13906+ Some(domain)
13907+ );
13908+ }
13909+
1364513910 #[test]
1364613911 fn macos_file_provider_approval_surface_path_uses_first_existing_candidate() {
1364713912 let temp = TestTempDir::new("approval-surface");
@@ -13676,6 +13941,9 @@ mod tests {
1367613941 ),
1367713942 expected
1367813943 );
13944+ if let Some(state) = expected {
13945+ assert_eq!(state.as_str(), "needs_finder_enable");
13946+ }
1367913947 }
1368013948
1368113949 #[test]
@@ -13716,7 +13984,7 @@ mod tests {
1371613984 super::workspace_mount_onboarding_curated_message(
1371713985 super::MacosWorkspaceMountOnboardingState::ApprovalRequired
1371813986 ),
13719- Some("Enable Locality in Finder, then return here and click Check again .")
13987+ Some("In Finder, click Enable for Locality. Locality will continue automatically .")
1372013988 );
1372113989 assert_eq!(
1372213990 super::workspace_mount_onboarding_curated_message(
@@ -17068,6 +17336,8 @@ fn main() {
1706817336 disconnect_source,
1706917337 export_source_backup,
1707017338 run_workspace_mount_onboarding,
17339+ file_provider_enablement_status,
17340+ reveal_file_provider_enablement,
1707117341 install_agent_guidance,
1707217342 locate_notion_page,
1707317343 search_notion_pages,
0 commit comments