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
299 let _ = CloseEmitter.emit("sky://window/close-requested", ());
300
301 dev_log!("window", "[UI] [Window] CloseRequested intercepted; forwarded to webview");
302 }
303 });
304 }
305
306 // -------------------------------------------------------------------------
307 // [Backend] [Dirs] Ensure userdata directories exist
308 // -------------------------------------------------------------------------
309 {
310 let PathResolver = app.path();
311
312 let AppDataDir = PathResolver.app_data_dir().unwrap_or_default();
313
314 let LogDir = PathResolver.app_log_dir().unwrap_or_default();
315
316 let HomeDir = PathResolver.home_dir().unwrap_or_default();
317
318 // Set the canonical userdata base so WindServiceHandlers resolves
319 // /User/... paths to the real Tauri app_data_dir (not hardcoded "FIDDEE").
320 crate::IPC::WindServiceHandlers::Utilities::UserdataDir::Set::Fn(AppDataDir.to_string_lossy().to_string());
321
322 // Set the real filesystem root for /Static/Application/ path mapping.
323 // In dev mode, Tauri serves from ../Sky/Target relative to Mountain.
324 // Tauri's resource_dir gives us the frontendDist path.
325 // Resolve Sky/Target via Tauri first; fall back to executable-
326 // relative bundle and monorepo layouts so raw-binary launches
327 // (e.g. running `Target/release/<bin>` directly from a terminal)
328 // still resolve `STATIC_APPLICATION_ROOT` correctly. Without this
329 // fallback, release binaries launched outside `.app` had an
330 // empty static root, causing extension-contributed icons served
331 // via `vscode-file://` to 404 (GitLens / Roo / Claude side bar
332 // icons missing).
333 let SkyTargetDir = PathResolver
334 .resource_dir()
335 .ok()
336 .filter(|P| !P.as_os_str().is_empty() && P.exists())
337 .unwrap_or_else(|| {
338 let ExeParent = std::env::current_exe()
339 .ok()
340 .and_then(|Exe| Exe.parent().map(|P| P.to_path_buf()))
341 .unwrap_or_default();
342
343 // `.app/Contents/MacOS/<bin>` → `Contents/Resources/`
344 let BundleResources = ExeParent.join("../Resources");
345
346 if BundleResources.exists() {
347 return BundleResources;
348 }
349
350 // Monorepo layout: `Element/Mountain/Target/<profile>/<bin>` →
351 // `Element/Sky/Target/`. Used by both debug runs and raw-
352 // release launches from inside the repo.
353 let RepoSky = ExeParent.join("../../../Sky/Target");
354
355 if RepoSky.exists() {
356 return RepoSky;
357 }
358
359 // Last resort: alongside the binary. A broken bundle layout
360 // then surfaces as visible "asset not found" 404s instead of
361 // silent empty-string joins.
362 ExeParent
363 });
364
365 crate::IPC::WindServiceHandlers::Utilities::ApplicationRoot::Set::Fn(
366 SkyTargetDir.to_string_lossy().to_string(),
367 );
368
369 dev_log!(
370 "lifecycle",
371 "[Lifecycle] [Dirs] Static application root: {}",
372 SkyTargetDir.display()
373 );
374
375 // Every directory VS Code may stat or readdir during startup
376 let Dirs = [
377 // User profile directories
378 AppDataDir.join("User"),
379 AppDataDir.join("User/globalStorage"),
380 AppDataDir.join("User/workspaceStorage"),
381 AppDataDir.join("User/workspaceStorage/vscode-chat-images"),
382 AppDataDir.join("User/extensions"),
383 AppDataDir.join("User/profiles/__default__profile__"),
384 AppDataDir.join("User/snippets"),
385 AppDataDir.join("User/prompts"),
386 AppDataDir.join("User/caches"),
387 // Configuration cache
388 AppDataDir.join("CachedConfigurations/defaults/__default__profile__-configurationDefaultsOverrides"),
389 // Log directories - VS Code stats {logsPath}/window1/output_{timestamp}
390 LogDir.join("window1"),
391 // System extensions directory - VS Code scans appRoot/../extensions
392 // which resolves to /Static/Application/extensions (mapped to Sky Target).
393 SkyTargetDir.join("Static/Application/extensions"),
394 // Agent directories VS Code probes for (create to avoid stat errors)
395 HomeDir.join(".claude/agents"),
396 HomeDir.join(".copilot/agents"),
397 ];
398
399 for Dir in &Dirs {
400 if let Err(Error) = std::fs::create_dir_all(Dir) {
401 dev_log!(
402 "lifecycle",
403 "warn: [Lifecycle] [Dirs] Failed to create {}: {}",
404 Dir.display(),
405 Error
406 );
407 }
408 }
409
410 // Default empty files VS Code reads on startup
411 let DefaultFiles:&[(&std::path::Path, &str)] = &[
412 (&AppDataDir.join("User/settings.json"), "{}"),
413 (&AppDataDir.join("User/keybindings.json"), "[]"),
414 (&AppDataDir.join("User/tasks.json"), "{}"),
415 (&AppDataDir.join("User/extensions.json"), "[]"),
416 (&AppDataDir.join("User/mcp.json"), "{}"),
417 ];
418
419 for (FilePath, DefaultContent) in DefaultFiles {
420 if !FilePath.exists() {
421 let _ = std::fs::write(FilePath, DefaultContent);
422 }
423 }
424
425 // Atom I7: ensure `security.workspace.trust.enabled: false` lives
426 // in User/settings.json. Without it, opening the Land repo as a
427 // workspace triggers VS Code's workspace-trust gate: built-in
428 // extensions whose `location` is inside the picked folder are
429 // marked `DisabledByTrustRequirement` (see
430 // `extensionEnablementService.ts:549`). Since our built-ins ship
431 // under `Element/Sky/Target/Static/Application/extensions/` -
432 // which IS inside the repo - any user picking the repo as a
433 // workspace hits this filter for every extension. Disabling the
434 // trust system wholesale is the correct Land-level policy; we're
435 // a personal editor, not a multi-user sandbox. Users can opt
436 // back in by flipping this key in their User/settings.json.
437 {
438 let SettingsPath = AppDataDir.join("User/settings.json");
439
440 let Current = std::fs::read_to_string(&SettingsPath).unwrap_or_else(|_| "{}".to_string());
441
442 if !Current.contains("\"security.workspace.trust.enabled\"") {
443 if let Ok(mut Parsed) = serde_json::from_str::<serde_json::Value>(&Current) {
444 if !Parsed.is_object() {
445 Parsed = serde_json::json!({});
446 }
447
448 if let Some(Obj) = Parsed.as_object_mut() {
449 Obj.insert("security.workspace.trust.enabled".to_string(), serde_json::Value::Bool(false));
450 }
451
452 if let Ok(Serialized) = serde_json::to_string_pretty(&Parsed) {
453 let _ = std::fs::write(&SettingsPath, Serialized);
454
455 dev_log!(
456 "lifecycle",
457 "[Lifecycle] [Dirs] Injected default 'security.workspace.trust.enabled=false' into {}",
458 SettingsPath.display()
459 );
460 }
461 }
462 }
463 }
464
465 // Set GlobalMementoPath now that we know the real Tauri app data dir
466 let GlobalMementoFile = AppDataDir.join("User/globalStorage/global.json");
467
468 if let Ok(mut Path) = app_state.GlobalMementoPath.lock() {
469 *Path = GlobalMementoFile.clone();
470 dev_log!("lifecycle", "[Lifecycle] [Dirs] GlobalMementoPath: {}", Path.display());
471 }
472
473 // Boot-time memento hydration: use the crash-safe best-effort loader.
474 // A corrupted global.json (partial write during a previous crash, disk
475 // corruption, manual edit gone wrong) gets quarantined to a timestamped
476 // `.json.corrupted.<ts>` sibling and the in-memory map starts empty
477 // rather than panicking the boot path. Workspace memento is loaded on
478 // `UpdateWorkspaceMementoPathAndReload` so we only hydrate global here.
479 {
480 let LoadedGlobal =
481 crate::ApplicationState::Internal::Persistence::MementoLoader::LoadInitialMementoFromDisk::Fn(
482 &GlobalMementoFile,
483 );
484
485 if !LoadedGlobal.is_empty() {
486 dev_log!(
487 "lifecycle",
488 "[Lifecycle] [Memento] Hydrated GlobalMemento ({} keys) from {}",
489 LoadedGlobal.len(),
490 GlobalMementoFile.display()
491 );
492 }
493
494 app_state.Configuration.SetGlobalMemento(LoadedGlobal);
495 }
496
497 dev_log!(
498 "lifecycle",
499 "[Lifecycle] [Dirs] Userdata directories ensured at {}",
500 AppDataDir.display()
501 );
502 }
503
504 // -------------------------------------------------------------------------
505 // [Backend] [Env] Mountain environment
506 // -------------------------------------------------------------------------
507 dev_log!("lifecycle", "[Backend] [Env] Creating MountainEnvironment...");
508
509 let Environment = Arc::new(MountainEnvironment::Create(app_handle_for_setup.clone(), app_state.clone()));
510
511 dev_log!("lifecycle", "[Backend] [Env] MountainEnvironment ready.");
512
513 // -------------------------------------------------------------------------
514 // [Backend] [Runtime] ApplicationRunTime
515 // -------------------------------------------------------------------------
516 dev_log!("lifecycle", "[Backend] [Runtime] Creating ApplicationRunTime...");
517
518 let Runtime = Arc::new(ApplicationRunTime::Create(scheduler.clone(), Environment.clone()));
519
520 app_handle_for_setup.manage(Runtime.clone());
521
522 dev_log!("lifecycle", "[Backend] [Runtime] ApplicationRunTime managed.");
523
524 // -------------------------------------------------------------------------
525 // [Lifecycle] [IPC] Initialize Status Reporter
526 // -------------------------------------------------------------------------
527 if let Err(e) = StatusReporterRegisterFn(&app_handle_for_setup, Runtime.clone()) {
528 dev_log!(
529 "lifecycle",
530 "error: [Lifecycle] [IPC] Failed to initialize status reporter: {}",
531 e
532 );
533 }
534
535 // -------------------------------------------------------------------------
536 // [Lifecycle] [IPC] Initialize Advanced Features
537 // -------------------------------------------------------------------------
538 if let Err(e) = AdvancedFeaturesRegisterFn(&app_handle_for_setup, Runtime.clone()) {
539 dev_log!(
540 "lifecycle",
541 "error: [Lifecycle] [IPC] Failed to initialize advanced features: {}",
542 e
543 );
544 }
545
546 // -------------------------------------------------------------------------
547 // [Lifecycle] [IPC] Initialize Wind Advanced Sync
548 // -------------------------------------------------------------------------
549 if let Err(e) = WindSyncRegisterFn(&app_handle_for_setup, Runtime.clone()) {
550 dev_log!(
551 "lifecycle",
552 "error: [Lifecycle] [IPC] Failed to initialize wind advanced sync: {}",
553 e
554 );
555 }
556
557 // -------------------------------------------------------------------------
558 // [Lifecycle] [PostSetup] Async initialization work
559 // -------------------------------------------------------------------------
560 let PostSetupAppHandle = app_handle_for_setup.clone();
561
562 let PostSetupEnvironment = Environment.clone();
563
564 tauri::async_runtime::spawn(async move {
565 dev_log!("lifecycle", "[Lifecycle] [PostSetup] Starting...");
566
567 let PostSetupStart = crate::IPC::DevLog::NowNano::Fn();
568
569 let AppStateForSetup = PostSetupEnvironment.ApplicationState.clone();
570
571 TraceStep!("[Lifecycle] [PostSetup] AppState cloned.");
572
573 // [Config]
574 // First-pass merge runs against the empty `ScannedExtensions`
575 // map (the scan happens later in this lifecycle). User /
576 // workspace `settings.json` overrides land here, but extension
577 // `contributes.configuration.properties[*].default` keys cannot
578 // be collected yet. Without a second pass after the scan,
579 // `getConfiguration('git').get('enabled')` returns undefined,
580 // vscode.git's `_activate` takes the `if (!enabled) return;`
581 // short-circuit, and the SCM viewlet stays empty even though
582 // Cocoon successfully activated the extension. The second pass
583 // below repairs this without disturbing the existing initial
584 // merge that the rest of bootstrap depends on.
585 let ConfigStart = crate::IPC::DevLog::NowNano::Fn();
586
587 let _ = ConfigurationInitializeFn(&PostSetupEnvironment).await;
588
589 crate::otel_span!("lifecycle:config:initialize", ConfigStart);
590
591 // [Workspace] [Trust] Desktop app - trust local workspace by default
592 AppStateForSetup.Workspace.SetTrustStatus(true);
593
594 // [Extensions] [ScanPaths]
595 let ExtScanStart = crate::IPC::DevLog::NowNano::Fn();
596
597 let _ = ScanPathConfigureFn(&AppStateForSetup);
598
599 // [Extensions] [Scan]
600 let _ = ExtensionPopulateFn(PostSetupAppHandle.clone(), &AppStateForSetup).await;
601
602 crate::otel_span!("lifecycle:extensions:scan", ExtScanStart);
603
604 // [Config] [Re-merge] - now that ScannedExtensions is populated,
605 // run the merge a second time so `collect_default_configurations`
606 // can walk extension manifests and seed `git.enabled = true`,
607 // `git.path = null`, `git.autoRepositoryDetection = true`, plus
608 // every other `contributes.configuration.properties[*].default`
609 // the 113 scanned extensions declare. The first-pass merge logged
610 // "0 top-level keys"; this pass should log a much larger count.
611 // User / workspace overrides applied during the first pass are
612 // preserved because the merge order is Default → User → Workspace
613 // and the cached User/Workspace JSON files are re-read each call.
614 let ConfigRemergeStart = crate::IPC::DevLog::NowNano::Fn();
615
616 let _ = ConfigurationInitializeFn(&PostSetupEnvironment).await;
617
618 crate::otel_span!("lifecycle:config:remerge-after-extension-scan", ConfigRemergeStart);
619
620 // [Vine] [gRPC]
621 let VineStart = crate::IPC::DevLog::NowNano::Fn();
622
623 let _ = VineStartFn(
624 PostSetupAppHandle.clone(),
625 "127.0.0.1:50051".to_string(),
626 "127.0.0.1:50052".to_string(),
627 )
628 .await;
629
630 crate::otel_span!("lifecycle:vine:start", VineStart);
631
632 // [Cocoon] [Sidecar] - skipped when Disable=true so the
633 // workbench loads without an extension host. Useful for
634 // bisecting whether typing-input regressions originate in
635 // Cocoon's gRPC handlers or upstream / Tauri / WKWebView.
636 if IsLandDisabled() {
637 dev_log!(
638 "cocoon",
639 "[Cocoon] [Start] Disable=true: Cocoon spawn SKIPPED (workbench will run without extensions)"
640 );
641 } else {
642 let CocoonStart = crate::IPC::DevLog::NowNano::Fn();
643
644 let _ = CocoonStartFn(&PostSetupAppHandle, &PostSetupEnvironment).await;
645
646 crate::otel_span!("lifecycle:cocoon:start", CocoonStart);
647 }
648
649 // [Air] [Sidecar] - daemon for updates / downloads / signing /
650 // indexing / system monitoring. Spawn parallel to Cocoon; both
651 // are sidecars in the Vine pool. AirStart returns Ok(()) even
652 // on spawn failure (graceful degradation - workbench works
653 // without Air, just without those background capabilities).
654 // Skipped under `Disable=true` for parity with Cocoon.
655 if IsLandDisabled() {
656 dev_log!("grpc", "[Air] [Start] Disable=true: Air spawn SKIPPED");
657 } else {
658 let AirStartT0 = crate::IPC::DevLog::NowNano::Fn();
659
660 let _ = AirStartFn(&PostSetupAppHandle, &PostSetupEnvironment).await;
661
662 crate::otel_span!("lifecycle:air:start", AirStartT0);
663 }
664
665 // [Lifecycle] [Phase] Advance Starting → Ready now that the gRPC
666 // server + Cocoon sidecar + extension scan have all finished. Wind's
667 // `TauriChannel("lifecycle").listen("onDidChangePhase")` subscribers
668 // fire so long-running services can start pulling.
669 AppStateForSetup.Feature.Lifecycle.AdvanceAndBroadcast(2, &PostSetupAppHandle);
670
671 // Schedule a background transition to Restored (3), then Eventually
672 // (4). Sky/Wind are the authoritative signal - they call
673 // `lifecycle:advancePhase` over Tauri IPC when the workbench is
674 // truly interactive (`Restored`) and when late-binding extensions
675 // should stop blocking (`Eventually`). `AdvanceAndBroadcast`
676 // rejects backwards/same-phase advances (LifecyclePhaseState.rs:53),
677 // so the timers below are pure fallbacks: if Sky has already driven
678 // the phase, these become no-ops and log nothing visible.
679 //
680 // The windows are deliberately generous - a debug-electron cold
681 // boot with 98 extensions has been observed to finish its
682 // `$activateByEvent("*")` burst at ~3.5 s on an M4 mini and
683 // noticeably later on older hardware. The previous 2 s / 5 s
684 // timings ran the risk of flipping Restored while the burst was
685 // still in flight, which prematurely unblocked services gated on
686 // "the editor is interactive". 8 s / 15 s keeps a safety margin
687 // without visibly delaying late-binding extensions that legitimately
688 // need Eventually to fire.
689 let LifecycleStateClone = AppStateForSetup.Feature.Lifecycle.clone();
690
691 let AppHandleForPhase = PostSetupAppHandle.clone();
692
693 tauri::async_runtime::spawn(async move {
694 tokio::time::sleep(tokio::time::Duration::from_millis(8_000)).await;
695
696 if LifecycleStateClone.GetPhase() < 3 {
697 dev_log!(
698 "lifecycle",
699 "[Lifecycle] [Fallback] Sky did not advance to Restored within 8s; Mountain auto-advancing \
700 (current phase={})",
701 LifecycleStateClone.GetPhase()
702 );
703
704 LifecycleStateClone.AdvanceAndBroadcast(3, &AppHandleForPhase);
705 }
706
707 tokio::time::sleep(tokio::time::Duration::from_millis(15_000)).await;
708
709 if LifecycleStateClone.GetPhase() < 4 {
710 dev_log!(
711 "lifecycle",
712 "[Lifecycle] [Fallback] Sky did not advance to Eventually within 23s total; Mountain \
713 auto-advancing (current phase={})",
714 LifecycleStateClone.GetPhase()
715 );
716
717 LifecycleStateClone.AdvanceAndBroadcast(4, &AppHandleForPhase);
718 }
719 });
720
721 // Hidden-until-ready safety timer: `WindowBuild.rs` creates the main
722 // window with `.visible(false)` and the `lifecycle:advancePhase(3)`
723 // handler reveals it once Sky reports the workbench DOM is attached.
724 // If Sky crashes before phase 3 reaches Mountain, the window would
725 // stay invisible forever. Force-reveal after 3 s so the user always
726 // sees SOMETHING even on a completely broken Sky. 3 s matches the
727 // observed p95 of `[Lifecycle] [Phase] Advance Ready` on a cold
728 // M-series boot, so the timer rarely fires on a healthy path.
729 let AppHandleForEmergencyShow = PostSetupAppHandle.clone();
730
731 tauri::async_runtime::spawn(async move {
732 tokio::time::sleep(tokio::time::Duration::from_millis(3_000)).await;
733
734 if let Some(MainWindow) = AppHandleForEmergencyShow.get_webview_window("main") {
735 if let Ok(false) = MainWindow.is_visible() {
736 dev_log!(
737 "lifecycle",
738 "warn: [Lifecycle] [Fallback] main window hidden at +3s; force-revealing to avoid an \
739 invisible-window lockup (Sky never reached phase 3)"
740 );
741
742 let _ = MainWindow.show();
743
744 let _ = MainWindow.set_focus();
745 }
746 }
747 });
748
749 crate::otel_span!("lifecycle:postsetup:complete", PostSetupStart);
750
751 dev_log!("lifecycle", "[Lifecycle] [PostSetup] Complete. System ready.");
752 });
753
754 Ok(())
755}