Skip to main content

Mountain/Binary/Main/
AppLifecycle.rs

1//! # AppLifecycle (Binary/Main)
2//!
3//! ## RESPONSIBILITIES
4//!
5//! Application lifecycle management for the Tauri application setup and
6//! initialization. This module handles the complete setup process during the
7//! Tauri setup hook, including tray initialization, command registration, IPC
8//! server setup, window creation, environment configuration, and async service
9//! initialization.
10//!
11//! ## ARCHITECTURAL ROLE
12//!
13//! The AppLifecycle module is the **initialization layer** in Mountain's
14//! architecture:
15//!
16//! ```text
17//! Tauri Builder Setup ──► AppLifecycle::AppLifecycleSetup()
18//!                              │
19//!                              ├─► Tray Initialization
20//!                              ├─► Command Registration
21//!                              ├─► IPC Server Setup
22//!                              ├─► Window Building
23//!                              ├─► Environment Setup
24//!                              ├─► Runtime Setup
25//!                              └─► Async Service Initialization
26//! ```
27//!
28//! ## KEY COMPONENTS
29//!
30//! - **AppLifecycleSetup()**: Main setup function orchestrating all
31//!   initialization
32//! - **Tray Initialization**: System tray icon with Dark/Light mode support
33//! - **Command Registration**: Native command registration with application
34//!   state
35//! - **IPC Server**: Mountain IPC server for frontend-backend communication
36//! - **Window Building**: Main application window configuration
37//! - **MountainEnvironment**: Environment context for application services
38//! - **ApplicationRunTime**: Runtime context with scheduler and environment
39//! - **Status Reporter**: IPC status reporting initialization
40//! - **Advanced Features**: Advanced IPC features initialization
41//! - **Wind Sync**: Wind advanced sync initialization
42//! - **Async Initialization**: Post-setup async service initialization
43//!
44//! ## ERROR HANDLING
45//!
46//! Returns `Result<(), Box<dyn std::error::Error>>` for setup errors.
47//! Non-critical failures are logged but don't prevent application startup.
48//! Critical failures are propagated to prevent incomplete startup.
49//!
50//! ## LOGGING
51//!
52//! Comprehensive logging at INFO level for major setup steps,
53//! DEBUG level for detailed processing, and ERROR for failures.
54//! All logs are prefixed with `[Lifecycle] [ComponentName]`.
55//!
56//! ## PERFORMANCE CONSIDERATIONS
57//!
58//! - Async initialization spawned after main setup to avoid blocking
59//! - Services initialized only when needed
60//! - Clone operations minimized for Arc-wrapped shared state
61//!
62//! ## TODO
63//! - [ ] Add setup progress tracking
64//! - [ ] Implement setup timeout handling
65//! - [ ] Add setup rollback mechanism on failure
66
67use std::sync::Arc;
68
69use tauri::Manager;
70use Echo::Scheduler::Scheduler::Scheduler;
71
72use crate::dev_log;
73
74/// Master "disable Land customisations" gate. Returns `true` when the
75/// `Disable=true` env var is set (PascalCase, single-word, matching
76/// the rest of Land's env surface in `.env.Land.Diagnostics`). When
77/// enabled, Mountain skips:
78///   - `WindowEvent::CloseRequested` intercept (Cmd+W routes natively)
79///   - Cocoon + Air sidecar spawn
80///   - The Wind / SkyBridge advanced-features registration
81///   - The smoke-test gating that would otherwise activate via Sky
82///
83/// Code paths are NOT removed - just skipped at runtime so a clean
84/// `Disable=` env var (or `Disable=false`) restores stock behaviour.
85fn IsLandDisabled() -> bool {
86	std::env::var("Disable")
87		.map(|Value| Value.eq_ignore_ascii_case("true"))
88		.unwrap_or(false)
89}
90use crate::{
91	// Crate root imports
92	ApplicationState::State::ApplicationState::ApplicationState,
93	// Binary submodule imports
94	Binary::Build::WindowBuild::WindowBuild as WindowBuildFn,
95	Binary::Extension::ExtensionPopulate::ExtensionPopulate as ExtensionPopulateFn,
96	Binary::Extension::ScanPathConfigure::ScanPathConfigure as ScanPathConfigureFn,
97	Binary::Register::AdvancedFeaturesRegister::AdvancedFeaturesRegister as AdvancedFeaturesRegisterFn,
98	Binary::Register::CommandRegister::CommandRegister as CommandRegisterFn,
99	Binary::Register::IPCServerRegister::IPCServerRegister as IPCServerRegisterFn,
100	Binary::Register::StatusReporterRegister::StatusReporterRegister as StatusReporterRegisterFn,
101	Binary::Register::WindSyncRegister::WindSyncRegister as WindSyncRegisterFn,
102	Binary::Service::AirStart::AirStart as AirStartFn,
103	Binary::Service::CocoonStart::CocoonStart as CocoonStartFn,
104	Binary::Service::ConfigurationInitialize::ConfigurationInitialize as ConfigurationInitializeFn,
105	Binary::Service::VineStart::VineStart as VineStartFn,
106	Binary::Tray::EnableTray as EnableTrayFn,
107	Environment::MountainEnvironment::MountainEnvironment,
108	RunTime::ApplicationRunTime::ApplicationRunTime,
109};
110
111/// Logs a checkpoint message at TRACE level.
112macro_rules! TraceStep {
113	($($arg:tt)*) => {{
114		dev_log!("lifecycle", $($arg)*);
115	}};
116}
117
118/// Sets up the application lifecycle during Tauri initialization.
119///
120/// This function coordinates all setup operations:
121/// 1. System tray initialization
122/// 2. Native command registration
123/// 3. IPC server initialization
124/// 4. Main window creation
125/// 5. Mountain environment setup
126/// 6. Application runtime setup
127/// 7. Status reporter initialization
128/// 8. Advanced features initialization
129/// 9. Wind advanced sync initialization
130/// 10. Async post-setup initialization
131///
132/// # Parameters
133///
134/// * `app` - Mutable reference to Tauri App instance
135/// * `app_handle` - Cloned Tauri AppHandle for async operations
136/// * `localhost_url` - URL for the development server
137/// * `scheduler` - Arc-wrapped Echo Scheduler
138/// * `app_state` - Application state clone
139///
140/// # Returns
141///
142/// `Result<(), Box<dyn std::error::Error>>` - Ok on success, Err on critical
143/// failure
144pub fn AppLifecycleSetup(
145	app:&mut tauri::App,
146	app_handle:tauri::AppHandle,
147	localhost_url:String,
148	scheduler:Arc<Scheduler>,
149	app_state:Arc<ApplicationState>,
150) -> Result<(), Box<dyn std::error::Error>> {
151	dev_log!("lifecycle", "[Lifecycle] [Setup] Setup hook started.");
152	dev_log!("lifecycle", "[Lifecycle] [Setup] LocalhostUrl={}", localhost_url);
153
154	let app_handle_for_setup = app_handle.clone();
155	TraceStep!("[Lifecycle] [Setup] AppHandle acquired.");
156
157	// -------------------------------------------------------------------------
158	// [UI] [Tray] Initialize System Tray
159	// -------------------------------------------------------------------------
160	dev_log!("lifecycle", "[UI] [Tray] Initializing system tray...");
161	if let Err(Error) = EnableTrayFn::enable_tray(app) {
162		dev_log!("lifecycle", "error: [UI] [Tray] Failed to enable tray: {}", Error);
163	}
164
165	// -------------------------------------------------------------------------
166	// [Lifecycle] [Commands] Register native commands
167	// -------------------------------------------------------------------------
168	dev_log!("lifecycle", "[Lifecycle] [Commands] Registering native commands...");
169	if let Err(e) = CommandRegisterFn(&app_handle_for_setup, &app_state) {
170		dev_log!("lifecycle", "error: [Lifecycle] [Commands] Failed to register commands: {}", e);
171	}
172	dev_log!("lifecycle", "[Lifecycle] [Commands] Native commands registered.");
173
174	// -------------------------------------------------------------------------
175	// [Lifecycle] [IPC] Initialize IPC Server
176	// -------------------------------------------------------------------------
177	dev_log!("lifecycle", "[Lifecycle] [IPC] Initializing Mountain IPC Server...");
178	if let Err(e) = IPCServerRegisterFn(&app_handle_for_setup) {
179		dev_log!("lifecycle", "error: [Lifecycle] [IPC] Failed to register IPC server: {}", e);
180	}
181
182	// -------------------------------------------------------------------------
183	// [UI] [Window] Build main window
184	// -------------------------------------------------------------------------
185	dev_log!("lifecycle", "[UI] [Window] Building main window...");
186	let MainWindow = WindowBuildFn(app, localhost_url.clone());
187	dev_log!("lifecycle", "[UI] [Window] Main window ready.");
188
189	// DevTools auto-open is opt-in via the PascalCase env var
190	// `Inspect=1` (or any non-empty value other than `0`). Naming
191	// follows Land's single-word PascalCase verb convention -
192	// see `.env.Land.Diagnostics` for the documented set.
193	//
194	// Auto-opening DevTools on every debug launch was the direct
195	// cause of "I can't type or fire keybindings": the DevTools
196	// window steals macOS keyboard focus the moment it appears, so
197	// the main webview never becomes first responder and every
198	// keystroke goes to DevTools (or the system menu) instead of
199	// the workbench. The keybinding shortcut `Cmd+Alt+I` (Tauri's
200	// default) and the right-click "Inspect" entry both still
201	// work when needed.
202	#[cfg(debug_assertions)]
203	{
204		let WantDevTools = std::env::var("Inspect")
205			.map(|Value| !Value.is_empty() && Value != "0")
206			.unwrap_or(false);
207		if WantDevTools {
208			dev_log!("lifecycle", "[UI] [Window] Inspect=1 set: opening DevTools.");
209			MainWindow.open_devtools();
210		} else {
211			dev_log!(
212				"lifecycle",
213				"[UI] [Window] Debug build: DevTools auto-open suppressed (export Inspect=1 to override)."
214			);
215		}
216	}
217
218	// -------------------------------------------------------------------------
219	// [UI] [Window] Intercept CloseRequested so Cmd+W (and the macOS app
220	// menu's Window > Close item) routes through the workbench instead of
221	// killing the whole window.
222	//
223	// On macOS, Tauri 2.x installs a default app menu that maps Cmd+W to
224	// NSWindow's `performClose:`. The webview's keydown handler never gets
225	// the event because the menu wins the responder chain. The result the
226	// user sees: hitting Cmd+W to close a tab nukes the entire editor.
227	//
228	// The fix is the standard Electron-style handshake:
229	//   1. Mountain prevents the close.
230	//   2. Mountain emits `sky://window/close-requested` to the webview.
231	//   3. Sky listens, asks the workbench to close the active editor; if there is
232	//      no active editor (or the workbench refuses), Sky calls
233	//      `nativeHost:closeWindow`, which uses `WebviewWindow::destroy()` to tear
234	//      the window down without re-firing CloseRequested.
235	if IsLandDisabled() {
236		dev_log!(
237			"window",
238			"[UI] [Window] Disable=true: CloseRequested intercept SKIPPED (Cmd+W will close window natively)"
239		);
240	} else {
241		use tauri::Emitter;
242		let CloseEmitter = MainWindow.clone();
243		MainWindow.on_window_event(move |Event| {
244			if let tauri::WindowEvent::CloseRequested { api, .. } = Event {
245				api.prevent_close();
246				let _ = CloseEmitter.emit("sky://window/close-requested", ());
247				dev_log!("window", "[UI] [Window] CloseRequested intercepted; forwarded to webview");
248			}
249		});
250	}
251
252	// -------------------------------------------------------------------------
253	// [Backend] [Dirs] Ensure userdata directories exist
254	// -------------------------------------------------------------------------
255	{
256		let PathResolver = app.path();
257		let AppDataDir = PathResolver.app_data_dir().unwrap_or_default();
258		let LogDir = PathResolver.app_log_dir().unwrap_or_default();
259		let HomeDir = PathResolver.home_dir().unwrap_or_default();
260
261		// Set the canonical userdata base so WindServiceHandlers resolves
262		// /User/... paths to the real Tauri app_data_dir (not hardcoded "Land").
263		crate::IPC::WindServiceHandlers::Utilities::UserdataDir::set_userdata_base_dir(
264			AppDataDir.to_string_lossy().to_string(),
265		);
266
267		// Set the real filesystem root for /Static/Application/ path mapping.
268		// In dev mode, Tauri serves from ../Sky/Target relative to Mountain.
269		// Tauri's resource_dir gives us the frontendDist path.
270		// Resolve Sky/Target via Tauri first; fall back to executable-
271		// relative bundle and monorepo layouts so raw-binary launches
272		// (e.g. running `Target/release/<bin>` directly from a terminal)
273		// still resolve `STATIC_APPLICATION_ROOT` correctly. Without this
274		// fallback, release binaries launched outside `.app` had an
275		// empty static root, causing extension-contributed icons served
276		// via `vscode-file://` to 404 (GitLens / Roo / Claude side bar
277		// icons missing).
278		let SkyTargetDir = PathResolver
279			.resource_dir()
280			.ok()
281			.filter(|P| !P.as_os_str().is_empty() && P.exists())
282			.unwrap_or_else(|| {
283				let ExeParent = std::env::current_exe()
284					.ok()
285					.and_then(|Exe| Exe.parent().map(|P| P.to_path_buf()))
286					.unwrap_or_default();
287
288				// `.app/Contents/MacOS/<bin>` → `Contents/Resources/`
289				let BundleResources = ExeParent.join("../Resources");
290				if BundleResources.exists() {
291					return BundleResources;
292				}
293
294				// Monorepo layout: `Element/Mountain/Target/<profile>/<bin>` →
295				// `Element/Sky/Target/`. Used by both debug runs and raw-
296				// release launches from inside the repo.
297				let RepoSky = ExeParent.join("../../../Sky/Target");
298				if RepoSky.exists() {
299					return RepoSky;
300				}
301
302				// Last resort: alongside the binary. A broken bundle layout
303				// then surfaces as visible "asset not found" 404s instead of
304				// silent empty-string joins.
305				ExeParent
306			});
307		crate::IPC::WindServiceHandlers::Utilities::ApplicationRoot::set_static_application_root(
308			SkyTargetDir.to_string_lossy().to_string(),
309		);
310		dev_log!(
311			"lifecycle",
312			"[Lifecycle] [Dirs] Static application root: {}",
313			SkyTargetDir.display()
314		);
315
316		// Every directory VS Code may stat or readdir during startup
317		let Dirs = [
318			// User profile directories
319			AppDataDir.join("User"),
320			AppDataDir.join("User/globalStorage"),
321			AppDataDir.join("User/workspaceStorage"),
322			AppDataDir.join("User/workspaceStorage/vscode-chat-images"),
323			AppDataDir.join("User/extensions"),
324			AppDataDir.join("User/profiles/__default__profile__"),
325			AppDataDir.join("User/snippets"),
326			AppDataDir.join("User/prompts"),
327			AppDataDir.join("User/caches"),
328			// Configuration cache
329			AppDataDir.join("CachedConfigurations/defaults/__default__profile__-configurationDefaultsOverrides"),
330			// Log directories - VS Code stats {logsPath}/window1/output_{timestamp}
331			LogDir.join("window1"),
332			// System extensions directory - VS Code scans appRoot/../extensions
333			// which resolves to /Static/Application/extensions (mapped to Sky Target).
334			SkyTargetDir.join("Static/Application/extensions"),
335			// Agent directories VS Code probes for (create to avoid stat errors)
336			HomeDir.join(".claude/agents"),
337			HomeDir.join(".copilot/agents"),
338		];
339		for Dir in &Dirs {
340			if let Err(Error) = std::fs::create_dir_all(Dir) {
341				dev_log!(
342					"lifecycle",
343					"warn: [Lifecycle] [Dirs] Failed to create {}: {}",
344					Dir.display(),
345					Error
346				);
347			}
348		}
349
350		// Default empty files VS Code reads on startup
351		let DefaultFiles:&[(&std::path::Path, &str)] = &[
352			(&AppDataDir.join("User/settings.json"), "{}"),
353			(&AppDataDir.join("User/keybindings.json"), "[]"),
354			(&AppDataDir.join("User/tasks.json"), "{}"),
355			(&AppDataDir.join("User/extensions.json"), "[]"),
356			(&AppDataDir.join("User/mcp.json"), "{}"),
357		];
358		for (FilePath, DefaultContent) in DefaultFiles {
359			if !FilePath.exists() {
360				let _ = std::fs::write(FilePath, DefaultContent);
361			}
362		}
363
364		// Atom I7: ensure `security.workspace.trust.enabled: false` lives
365		// in User/settings.json. Without it, opening the Land repo as a
366		// workspace triggers VS Code's workspace-trust gate: built-in
367		// extensions whose `location` is inside the picked folder are
368		// marked `DisabledByTrustRequirement` (see
369		// `extensionEnablementService.ts:549`). Since our built-ins ship
370		// under `Element/Sky/Target/Static/Application/extensions/` -
371		// which IS inside the repo - any user picking the repo as a
372		// workspace hits this filter for every extension. Disabling the
373		// trust system wholesale is the correct Land-level policy; we're
374		// a personal editor, not a multi-user sandbox. Users can opt
375		// back in by flipping this key in their User/settings.json.
376		{
377			let SettingsPath = AppDataDir.join("User/settings.json");
378			let Current = std::fs::read_to_string(&SettingsPath).unwrap_or_else(|_| "{}".to_string());
379			if !Current.contains("\"security.workspace.trust.enabled\"") {
380				if let Ok(mut Parsed) = serde_json::from_str::<serde_json::Value>(&Current) {
381					if !Parsed.is_object() {
382						Parsed = serde_json::json!({});
383					}
384					if let Some(Obj) = Parsed.as_object_mut() {
385						Obj.insert("security.workspace.trust.enabled".to_string(), serde_json::Value::Bool(false));
386					}
387					if let Ok(Serialized) = serde_json::to_string_pretty(&Parsed) {
388						let _ = std::fs::write(&SettingsPath, Serialized);
389						dev_log!(
390							"lifecycle",
391							"[Lifecycle] [Dirs] Injected default 'security.workspace.trust.enabled=false' into {}",
392							SettingsPath.display()
393						);
394					}
395				}
396			}
397		}
398
399		// Set GlobalMementoPath now that we know the real Tauri app data dir
400		if let Ok(mut Path) = app_state.GlobalMementoPath.lock() {
401			*Path = AppDataDir.join("User/globalStorage/global.json");
402			dev_log!("lifecycle", "[Lifecycle] [Dirs] GlobalMementoPath: {}", Path.display());
403		}
404		dev_log!(
405			"lifecycle",
406			"[Lifecycle] [Dirs] Userdata directories ensured at {}",
407			AppDataDir.display()
408		);
409	}
410
411	// -------------------------------------------------------------------------
412	// [Backend] [Env] Mountain environment
413	// -------------------------------------------------------------------------
414	dev_log!("lifecycle", "[Backend] [Env] Creating MountainEnvironment...");
415	let Environment = Arc::new(MountainEnvironment::Create(app_handle_for_setup.clone(), app_state.clone()));
416	dev_log!("lifecycle", "[Backend] [Env] MountainEnvironment ready.");
417
418	// -------------------------------------------------------------------------
419	// [Backend] [Runtime] ApplicationRunTime
420	// -------------------------------------------------------------------------
421	dev_log!("lifecycle", "[Backend] [Runtime] Creating ApplicationRunTime...");
422	let Runtime = Arc::new(ApplicationRunTime::Create(scheduler.clone(), Environment.clone()));
423	app_handle_for_setup.manage(Runtime.clone());
424	dev_log!("lifecycle", "[Backend] [Runtime] ApplicationRunTime managed.");
425
426	// -------------------------------------------------------------------------
427	// [Lifecycle] [IPC] Initialize Status Reporter
428	// -------------------------------------------------------------------------
429	if let Err(e) = StatusReporterRegisterFn(&app_handle_for_setup, Runtime.clone()) {
430		dev_log!(
431			"lifecycle",
432			"error: [Lifecycle] [IPC] Failed to initialize status reporter: {}",
433			e
434		);
435	}
436
437	// -------------------------------------------------------------------------
438	// [Lifecycle] [IPC] Initialize Advanced Features
439	// -------------------------------------------------------------------------
440	if let Err(e) = AdvancedFeaturesRegisterFn(&app_handle_for_setup, Runtime.clone()) {
441		dev_log!(
442			"lifecycle",
443			"error: [Lifecycle] [IPC] Failed to initialize advanced features: {}",
444			e
445		);
446	}
447
448	// -------------------------------------------------------------------------
449	// [Lifecycle] [IPC] Initialize Wind Advanced Sync
450	// -------------------------------------------------------------------------
451	if let Err(e) = WindSyncRegisterFn(&app_handle_for_setup, Runtime.clone()) {
452		dev_log!(
453			"lifecycle",
454			"error: [Lifecycle] [IPC] Failed to initialize wind advanced sync: {}",
455			e
456		);
457	}
458
459	// -------------------------------------------------------------------------
460	// [Lifecycle] [PostSetup] Async initialization work
461	// -------------------------------------------------------------------------
462	let PostSetupAppHandle = app_handle_for_setup.clone();
463	let PostSetupEnvironment = Environment.clone();
464
465	tauri::async_runtime::spawn(async move {
466		dev_log!("lifecycle", "[Lifecycle] [PostSetup] Starting...");
467		let PostSetupStart = crate::IPC::DevLog::NowNano();
468		let AppStateForSetup = PostSetupEnvironment.ApplicationState.clone();
469		TraceStep!("[Lifecycle] [PostSetup] AppState cloned.");
470
471		// [Config]
472		// First-pass merge runs against the empty `ScannedExtensions`
473		// map (the scan happens later in this lifecycle). User /
474		// workspace `settings.json` overrides land here, but extension
475		// `contributes.configuration.properties[*].default` keys cannot
476		// be collected yet. Without a second pass after the scan,
477		// `getConfiguration('git').get('enabled')` returns undefined,
478		// vscode.git's `_activate` takes the `if (!enabled) return;`
479		// short-circuit, and the SCM viewlet stays empty even though
480		// Cocoon successfully activated the extension. The second pass
481		// below repairs this without disturbing the existing initial
482		// merge that the rest of bootstrap depends on.
483		let ConfigStart = crate::IPC::DevLog::NowNano();
484		let _ = ConfigurationInitializeFn(&PostSetupEnvironment).await;
485		crate::otel_span!("lifecycle:config:initialize", ConfigStart);
486
487		// [Workspace] [Trust] Desktop app - trust local workspace by default
488		AppStateForSetup.Workspace.SetTrustStatus(true);
489
490		// [Extensions] [ScanPaths]
491		let ExtScanStart = crate::IPC::DevLog::NowNano();
492		let _ = ScanPathConfigureFn(&AppStateForSetup);
493
494		// [Extensions] [Scan]
495		let _ = ExtensionPopulateFn(PostSetupAppHandle.clone(), &AppStateForSetup).await;
496		crate::otel_span!("lifecycle:extensions:scan", ExtScanStart);
497
498		// [Config] [Re-merge] - now that ScannedExtensions is populated,
499		// run the merge a second time so `collect_default_configurations`
500		// can walk extension manifests and seed `git.enabled = true`,
501		// `git.path = null`, `git.autoRepositoryDetection = true`, plus
502		// every other `contributes.configuration.properties[*].default`
503		// the 113 scanned extensions declare. The first-pass merge logged
504		// "0 top-level keys"; this pass should log a much larger count.
505		// User / workspace overrides applied during the first pass are
506		// preserved because the merge order is Default → User → Workspace
507		// and the cached User/Workspace JSON files are re-read each call.
508		let ConfigRemergeStart = crate::IPC::DevLog::NowNano();
509		let _ = ConfigurationInitializeFn(&PostSetupEnvironment).await;
510		crate::otel_span!("lifecycle:config:remerge-after-extension-scan", ConfigRemergeStart);
511
512		// [Vine] [gRPC]
513		let VineStart = crate::IPC::DevLog::NowNano();
514		let _ = VineStartFn(
515			PostSetupAppHandle.clone(),
516			"127.0.0.1:50051".to_string(),
517			"127.0.0.1:50052".to_string(),
518		)
519		.await;
520		crate::otel_span!("lifecycle:vine:start", VineStart);
521
522		// [Cocoon] [Sidecar] - skipped when Disable=true so the
523		// workbench loads without an extension host. Useful for
524		// bisecting whether typing-input regressions originate in
525		// Cocoon's gRPC handlers or upstream / Tauri / WKWebView.
526		if IsLandDisabled() {
527			dev_log!(
528				"cocoon",
529				"[Cocoon] [Start] Disable=true: Cocoon spawn SKIPPED (workbench will run without extensions)"
530			);
531		} else {
532			let CocoonStart = crate::IPC::DevLog::NowNano();
533			let _ = CocoonStartFn(&PostSetupAppHandle, &PostSetupEnvironment).await;
534			crate::otel_span!("lifecycle:cocoon:start", CocoonStart);
535		}
536
537		// [Air] [Sidecar] - daemon for updates / downloads / signing /
538		// indexing / system monitoring. Spawn parallel to Cocoon; both
539		// are sidecars in the Vine pool. AirStart returns Ok(()) even
540		// on spawn failure (graceful degradation - workbench works
541		// without Air, just without those background capabilities).
542		// Skipped under `Disable=true` for parity with Cocoon.
543		if IsLandDisabled() {
544			dev_log!("grpc", "[Air] [Start] Disable=true: Air spawn SKIPPED");
545		} else {
546			let AirStartT0 = crate::IPC::DevLog::NowNano();
547			let _ = AirStartFn(&PostSetupAppHandle, &PostSetupEnvironment).await;
548			crate::otel_span!("lifecycle:air:start", AirStartT0);
549		}
550
551		// [Lifecycle] [Phase] Advance Starting → Ready now that the gRPC
552		// server + Cocoon sidecar + extension scan have all finished. Wind's
553		// `TauriChannel("lifecycle").listen("onDidChangePhase")` subscribers
554		// fire so long-running services can start pulling.
555		AppStateForSetup.Feature.Lifecycle.AdvanceAndBroadcast(2, &PostSetupAppHandle);
556
557		// Schedule a background transition to Restored (3), then Eventually
558		// (4). Sky/Wind are the authoritative signal - they call
559		// `lifecycle:advancePhase` over Tauri IPC when the workbench is
560		// truly interactive (`Restored`) and when late-binding extensions
561		// should stop blocking (`Eventually`). `AdvanceAndBroadcast`
562		// rejects backwards/same-phase advances (LifecyclePhaseState.rs:53),
563		// so the timers below are pure fallbacks: if Sky has already driven
564		// the phase, these become no-ops and log nothing visible.
565		//
566		// The windows are deliberately generous - a debug-electron cold
567		// boot with 98 extensions has been observed to finish its
568		// `$activateByEvent("*")` burst at ~3.5 s on an M4 mini and
569		// noticeably later on older hardware. The previous 2 s / 5 s
570		// timings ran the risk of flipping Restored while the burst was
571		// still in flight, which prematurely unblocked services gated on
572		// "the editor is interactive". 8 s / 15 s keeps a safety margin
573		// without visibly delaying late-binding extensions that legitimately
574		// need Eventually to fire.
575		let LifecycleStateClone = AppStateForSetup.Feature.Lifecycle.clone();
576		let AppHandleForPhase = PostSetupAppHandle.clone();
577		tauri::async_runtime::spawn(async move {
578			tokio::time::sleep(tokio::time::Duration::from_millis(8_000)).await;
579			if LifecycleStateClone.GetPhase() < 3 {
580				dev_log!(
581					"lifecycle",
582					"[Lifecycle] [Fallback] Sky did not advance to Restored within 8s; Mountain auto-advancing \
583					 (current phase={})",
584					LifecycleStateClone.GetPhase()
585				);
586				LifecycleStateClone.AdvanceAndBroadcast(3, &AppHandleForPhase);
587			}
588			tokio::time::sleep(tokio::time::Duration::from_millis(15_000)).await;
589			if LifecycleStateClone.GetPhase() < 4 {
590				dev_log!(
591					"lifecycle",
592					"[Lifecycle] [Fallback] Sky did not advance to Eventually within 23s total; Mountain \
593					 auto-advancing (current phase={})",
594					LifecycleStateClone.GetPhase()
595				);
596				LifecycleStateClone.AdvanceAndBroadcast(4, &AppHandleForPhase);
597			}
598		});
599
600		// Hidden-until-ready safety timer: `WindowBuild.rs` creates the main
601		// window with `.visible(false)` and the `lifecycle:advancePhase(3)`
602		// handler reveals it once Sky reports the workbench DOM is attached.
603		// If Sky crashes before phase 3 reaches Mountain, the window would
604		// stay invisible forever. Force-reveal after 3 s so the user always
605		// sees SOMETHING even on a completely broken Sky. 3 s matches the
606		// observed p95 of `[Lifecycle] [Phase] Advance Ready` on a cold
607		// M-series boot, so the timer rarely fires on a healthy path.
608		let AppHandleForEmergencyShow = PostSetupAppHandle.clone();
609		tauri::async_runtime::spawn(async move {
610			tokio::time::sleep(tokio::time::Duration::from_millis(3_000)).await;
611			if let Some(MainWindow) = AppHandleForEmergencyShow.get_webview_window("main") {
612				if let Ok(false) = MainWindow.is_visible() {
613					dev_log!(
614						"lifecycle",
615						"warn: [Lifecycle] [Fallback] main window hidden at +3s; force-revealing to avoid an \
616						 invisible-window lockup (Sky never reached phase 3)"
617					);
618					let _ = MainWindow.show();
619					let _ = MainWindow.set_focus();
620				}
621			}
622		});
623
624		crate::otel_span!("lifecycle:postsetup:complete", PostSetupStart);
625		dev_log!("lifecycle", "[Lifecycle] [PostSetup] Complete. System ready.");
626	});
627
628	Ok(())
629}