Skip to main content

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