@@ -3,7 +3,10 @@ use serde::Serialize;
33use tauri:: { AppHandle , Emitter , State } ;
44
55use crate :: app_state:: AppState ;
6- use crate :: managed_agents:: { ensure_repos_symlink, nest_dir, try_regenerate_nest} ;
6+ use crate :: managed_agents:: {
7+ effective_repos_dir, ensure_repos_symlink, nest_dir, try_regenerate_nest,
8+ write_persisted_repos_dir,
9+ } ;
710use crate :: relay;
811
912#[ derive( Serialize ) ]
@@ -23,18 +26,36 @@ pub fn get_active_workspace(state: State<'_, AppState>) -> Result<ActiveWorkspac
2326 } )
2427}
2528
29+ /// Validate a candidate `repos_dir` without mutating the filesystem.
30+ ///
31+ /// The Add/Edit workspace dialogs call this on submit to block Save on a bad
32+ /// path, so a typo never reaches `apply_workspace`. Reuses the same
33+ /// `validate_repos_dir` the boot/apply path uses — one source of truth for
34+ /// "what's a valid repos dir". An empty/whitespace value clears the override
35+ /// and is valid. `Err` carries the human-readable reason for inline display.
36+ #[ tauri:: command]
37+ pub fn validate_repos_dir ( dir : String ) -> Result < ( ) , String > {
38+ let trimmed = dir. trim ( ) ;
39+ if trimmed. is_empty ( ) {
40+ return Ok ( ( ) ) ;
41+ }
42+ let nest = nest_dir ( ) . ok_or ( "cannot resolve home directory for nest" ) ?;
43+ crate :: managed_agents:: validate_repos_dir ( & nest, trimmed) . map ( |_| ( ) )
44+ }
45+
2646/// Apply a workspace's configuration to the backend session.
2747///
2848/// Called by the frontend on app init (after reload) to configure the
2949/// Tauri backend with the selected workspace's relay URL, keys, and repos
3050/// directory.
3151///
32- /// Validation runs before any state mutation: an invalid `repos_dir` (bad
33- /// path) rejects cleanly with nothing applied. The `REPOS` symlink itself is
34- /// a filesystem *side-effect* — its failure (e.g. a non-empty real `REPOS`
35- /// refusing a downgrade, or a renamed external target on a later launch) is
36- /// non-fatal: relay/keys still apply, the command returns `Ok`, and a
37- /// `repos-dir-error` event surfaces the failure to the frontend.
52+ /// A bad `repos_dir` is non-fatal: relay/keys always apply (the relay is the
53+ /// active workspace's own choice — orthogonal to the filesystem repos dir),
54+ /// the bad value is NOT persisted (so the next boot starts clean), the
55+ /// `REPOS` symlink is skipped (REPOS stays a real dir), a `repos-dir-error`
56+ /// event surfaces the reason, and the command returns `Ok`. The dialogs
57+ /// already block a bad path at Save (`validate_repos_dir`); this fallback only
58+ /// catches a value that went bad after save (deleted dir, unmounted volume).
3859#[ tauri:: command]
3960pub fn apply_workspace (
4061 relay_url : String ,
@@ -51,23 +72,25 @@ pub fn apply_workspace(
5172 None => None ,
5273 } ;
5374
54- // Normalize repos_dir to a trimmed non-empty value. `None`/empty clears
55- // the override (REPOS falls back to a real dir). A bad path is rejected
56- // here — before any mutation — so the dialog sees a clean Err.
57- let repos_dir = repos_dir
58- . map ( |s| s. trim ( ) . to_string ( ) )
59- . filter ( |s| !s. is_empty ( ) ) ;
60- if let Some ( dir) = repos_dir. as_deref ( ) {
61- let nest = nest_dir ( ) . ok_or ( "cannot resolve home directory for nest" ) ?;
62- // Validate without mutating the filesystem. Keeps the command's
63- // "validate-first, nothing below can fail" contract honest. Also emit
64- // the error so it surfaces even at the init call site (which swallows
65- // the returned Err to console for the relay/keys path).
66- if let Err ( error) = crate :: managed_agents:: validate_repos_dir ( & nest, dir) {
67- let _ = app. emit ( "repos-dir-error" , error. clone ( ) ) ;
68- return Err ( error) ;
69- }
70- }
75+ // Decide the effective repos_dir from the candidate. A bad path does NOT
76+ // reject — it is treated as if no override were set: relay/keys still
77+ // apply, the bad value is not persisted, and a `repos-dir-error` surfaces
78+ // the reason. Persisting a bad path would make every later boot read it,
79+ // fail to resolve the symlink, and silently skip agent restore. One
80+ // validate (inside `effective_repos_dir`) drives both the emit and the
81+ // persisted value. `nest` is resolved softly: when absent there is nothing
82+ // to persist or symlink, and relay/keys must still apply unconditionally.
83+ let nest = nest_dir ( ) ;
84+ let effective_repos_dir = match nest. as_deref ( ) {
85+ Some ( nest) => match effective_repos_dir ( nest, repos_dir. as_deref ( ) ) {
86+ Ok ( value) => value,
87+ Err ( error) => {
88+ let _ = app. emit ( "repos-dir-error" , error) ;
89+ None
90+ }
91+ } ,
92+ None => None ,
93+ } ;
7194
7295 // ── Apply all state changes (nothing below can fail) ──────────────────
7396 {
@@ -81,11 +104,20 @@ pub fn apply_workspace(
81104 }
82105
83106 // ── Filesystem side-effect (non-fatal) ────────────────────────────────
84- // Re-point REPOS to match repos_dir. Failure here (downgrade refused,
85- // external target gone) must NOT fail the command — relay/keys are already
86- // applied. Surface it via a `repos-dir-error` event the frontend toasts.
87- if let Some ( nest) = nest_dir ( ) {
88- if let Err ( error) = ensure_repos_symlink ( & nest, repos_dir. as_deref ( ) ) {
107+ // Persist the *effective* repos_dir (None when the candidate failed
108+ // validation) for the backend to read at boot, then re-point REPOS to
109+ // match. Persisting first makes the dotfile authoritative even if the
110+ // symlink apply fails here (e.g. a non-empty real REPOS): the next boot
111+ // reads the persisted value and resolves the symlink before any agent can
112+ // clone into REPOS. A bad candidate persists `None`, so the next boot is
113+ // clean and agent restore proceeds. Failure of either must NOT fail the
114+ // command — relay/keys are already applied. Surface symlink errors via
115+ // `repos-dir-error`.
116+ if let Some ( nest) = nest. as_deref ( ) {
117+ if let Err ( error) = write_persisted_repos_dir ( nest, effective_repos_dir. as_deref ( ) ) {
118+ eprintln ! ( "buzz-desktop: persist repos dir failed: {error}" ) ;
119+ }
120+ if let Err ( error) = ensure_repos_symlink ( nest, effective_repos_dir. as_deref ( ) ) {
89121 eprintln ! ( "buzz-desktop: repos dir setup failed: {error}" ) ;
90122 let _ = app. emit ( "repos-dir-error" , error) ;
91123 }
0 commit comments