Skip to content

Commit 993a69a

Browse files
feat(Mountain): Implement hidden-until-ready window pattern
Eliminate the visual flash on startup by creating the main window with .visible(false) and revealing it only when Sky reports phase 3 (Restored). The previous approach allowed Tauri's default immediate visibility, causing a four-repaint cascade: native chrome → #1e1e1e inline background → VS Code theme CSS → workbench DOM, observed as "purple/dark flash" and panel-pop flicker over ~200ms. Changes: - WindowBuild.rs: Add .visible(false) to WebviewWindowBuilder chain - AppLifecycle.rs: Add 3s safety timer to force-reveal if Sky crashes before phase 3 (matches observed cold-boot p95 timing) - WindServiceHandlers/mod.rs: Show window on phase 3 + set_focus() - CommandProvider.rs: Handle missing workbench commands as silent no-ops (getTelemetrySenderObject, testing.clearTestResults) - DevLog.rs: Document Batch 4 fs-route/cmd-route diagnostic tags - CocoonManagement.rs: Forward LAND_* env vars to Cocoon (fixes PostHog bridge fallback) The 3s safety timer guarantees the window appears even if Sky crashes before signalling phase 3, preventing invisible-window lockup while rarely firing on a healthy startup path.
1 parent 5140a38 commit 993a69a

6 files changed

Lines changed: 145 additions & 10 deletions

File tree

Source/Binary/Build/WindowBuild.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,19 @@ pub fn WindowBuild(Application:&mut App, LocalhostUrl:String) -> tauri::WebviewW
4848
.expect("FATAL: Failed to parse initial webview URL"),
4949
);
5050

51-
// Configure window builder with base settings
51+
// Configure window builder with base settings.
52+
//
53+
// `visible(false)` is the hidden-until-ready pattern. Tauri's default
54+
// is to show the window the instant it's built, which paints the native
55+
// chrome + Base.astro's `#1e1e1e` inline background + VS Code theme CSS
56+
// + workbench DOM in four separate repaints over the first ~200 ms -
57+
// observed as the "purple/dark flash" and panel-pop flicker.
58+
//
59+
// Mountain shows the window explicitly when the frontend's
60+
// `lifecycle:advancePhase(3)` (Restored) arrives, which fires after
61+
// `.monaco-workbench` is attached and the first frame is ready. A 3 s
62+
// safety timer in `AppLifecycle` guarantees the window appears even if
63+
// Sky crashes before signalling phase 3.
5264
let mut WindowBuilder = WebviewWindowBuilder::new(Application, "main", WindowUrl)
5365
.use_https_scheme(false)
5466
.initialization_script("")
@@ -57,7 +69,8 @@ pub fn WindowBuild(Application:&mut App, LocalhostUrl:String) -> tauri::WebviewW
5769
.title("Mountain")
5870
.resizable(true)
5971
.inner_size(1400.0, 900.0)
60-
.shadow(true);
72+
.shadow(true)
73+
.visible(false);
6174

6275
#[cfg(target_os = "macos")]
6376
{

Source/Binary/Main/AppLifecycle.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,30 @@ pub fn AppLifecycleSetup(
451451
}
452452
});
453453

454+
// Hidden-until-ready safety timer: `WindowBuild.rs` creates the main
455+
// window with `.visible(false)` and the `lifecycle:advancePhase(3)`
456+
// handler reveals it once Sky reports the workbench DOM is attached.
457+
// If Sky crashes before phase 3 reaches Mountain, the window would
458+
// stay invisible forever. Force-reveal after 3 s so the user always
459+
// sees SOMETHING even on a completely broken Sky. 3 s matches the
460+
// observed p95 of `[Lifecycle] [Phase] Advance Ready` on a cold
461+
// M-series boot, so the timer rarely fires on a healthy path.
462+
let AppHandleForEmergencyShow = PostSetupAppHandle.clone();
463+
tauri::async_runtime::spawn(async move {
464+
tokio::time::sleep(tokio::time::Duration::from_millis(3_000)).await;
465+
if let Some(MainWindow) = AppHandleForEmergencyShow.get_webview_window("main") {
466+
if let Ok(false) = MainWindow.is_visible() {
467+
dev_log!(
468+
"lifecycle",
469+
"warn: [Lifecycle] [Fallback] main window hidden at +3s; force-revealing to avoid an \
470+
invisible-window lockup (Sky never reached phase 3)"
471+
);
472+
let _ = MainWindow.show();
473+
let _ = MainWindow.set_focus();
474+
}
475+
}
476+
});
477+
454478
crate::otel_span!("lifecycle:postsetup:complete", PostSetupStart);
455479
dev_log!("lifecycle", "[Lifecycle] [PostSetup] Complete. System ready.");
456480
});

Source/Environment/CommandProvider.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,38 @@ impl CommandExecutor for MountainEnvironment {
300300
return Ok(Value::Null);
301301
}
302302

303+
// Workbench-internal commands that stock VS Code registers on
304+
// the renderer side via `CommandsRegistry.registerCommand(…)`
305+
// but that Land doesn't carry because the backing service
306+
// doesn't exist:
307+
//
308+
// - `getTelemetrySenderObject` - `vs/platform/telemetry/**`
309+
// registers this so extensions can fetch a `TelemetrySender`
310+
// via `commands.executeCommand`. Land has no telemetry
311+
// backend, so returning null (no sender) matches the
312+
// "telemetry disabled" code path every extension already
313+
// defensively handles.
314+
// - `testing.clearTestResults` - registered by
315+
// `vs/workbench/contrib/testing/browser/testExplorerActions.ts`.
316+
// No test-explorer UI in Land today; null is the correct
317+
// "nothing to clear" shape.
318+
//
319+
// Extensions that look these up defensively try/catch. The
320+
// only observable effect of the prior error return was the
321+
// red `error:` log line. Treat as silent no-ops until Land
322+
// grows the corresponding services.
323+
if matches!(
324+
CommandIdentifier.as_str(),
325+
"getTelemetrySenderObject" | "testing.clearTestResults"
326+
) {
327+
dev_log!(
328+
"commands",
329+
"[CommandProvider] Workbench-internal command '{}' not registered; treating as no-op (Land has no backing service).",
330+
CommandIdentifier
331+
);
332+
return Ok(Value::Null);
333+
}
334+
303335
dev_log!(
304336
"commands",
305337
"error: [CommandProvider] Command '{}' not found in registry.",

Source/IPC/DevLog.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@
114114
//! | `preload-shim` | Wind `Preload.ts` globals wiring, VS Code `ipcRenderer` polyfill install|
115115
//! | `tauri-invoke` | Per-invoke method + duration (augments `ipc`'s paired invoke/done lines)|
116116
//! | `bootstrap-stage` | Cocoon `Effect/Bootstrap.ts` stage timings (start/ok/fail per phase) |
117+
//!
118+
//! ### Batch 4 diagnostic tags
119+
//!
120+
//! Added 2026-04-24 alongside the `workspace.fs` tier-split refactor.
121+
//! Cocoon's `WorkspaceNamespace/FileSystemRoute.ts` now chooses between
122+
//! Tier A (`node:fs/promises` in-process) and Tier C (Mountain `FileSystem.*`
123+
//! gRPC) per URI scheme + custom-provider claim. The tag surfaces every
124+
//! decision so empirical workload profiling confirms the split is paying
125+
//! off - `grep 'route=native'` / `grep 'route=mountain'` buckets per run.
126+
//! Emitted from Cocoon stdout, picked up by Mountain's `[DEV:COCOON]`
127+
//! stdout tail with the standard `[DEV:FS-ROUTE]` prefix.
128+
//!
129+
//! | Tag | Scope |
130+
//! |--------------------|-------------------------------------------------------------------------|
131+
//! | `fs-route` | `workspace.fs.*` + `openTextDocument` native-vs-mountain routing |
132+
//! | `cmd-route` | `commands.executeCommand` local-vs-mountain routing |
117133
118134
use std::{
119135
fs::{File, OpenOptions, create_dir_all},

Source/IPC/WindServiceHandlers/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,42 @@ pub async fn mountain_ipc_invoke(app_handle:AppHandle, command:String, args:Vec<
722722
.Feature
723723
.Lifecycle
724724
.AdvanceAndBroadcast(NewPhase, &app_handle);
725+
726+
// Hidden-until-ready: the main window is built with
727+
// `.visible(false)` to suppress the four-repaint flash
728+
// (native chrome → inline bg → theme CSS → workbench
729+
// DOM). Phase 3 = Restored means `.monaco-workbench`
730+
// is attached and the first frame is painted; show
731+
// the window now so the user's first glimpse is the
732+
// finished editor rather than the paint cascade.
733+
//
734+
// `set_focus()` follows `show()` so keyboard input
735+
// routes to the editor immediately on reveal.
736+
// Failures are logged but swallowed - if the window
737+
// is already visible (phase 3 re-fired from another
738+
// consumer) Tauri returns a benign error.
739+
if NewPhase >= 3 {
740+
if let Some(MainWindow) = app_handle.get_webview_window("main") {
741+
if let Ok(false) = MainWindow.is_visible() {
742+
if let Err(Error) = MainWindow.show() {
743+
dev_log!(
744+
"lifecycle",
745+
"warn: [Lifecycle] main window show() failed on phase {}: {}",
746+
NewPhase,
747+
Error
748+
);
749+
} else {
750+
dev_log!(
751+
"lifecycle",
752+
"[Lifecycle] main window revealed on phase {} (hidden-until-ready)",
753+
NewPhase
754+
);
755+
let _ = MainWindow.set_focus();
756+
}
757+
}
758+
}
759+
}
760+
725761
Ok(json!(runtime.Environment.ApplicationState.Feature.Lifecycle.GetPhase()))
726762
},
727763

Source/ProcessManagement/CocoonManagement.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -317,19 +317,33 @@ async fn LaunchAndManageCocoonSideCar(
317317
// identity, and port configuration. Without this forwarding, the
318318
// whitelist above drops them and Cocoon falls back to defaults,
319319
// defeating the single-source-of-truth design.
320+
//
321+
// `LAND_*` prefix: covers `.env.Land.PostHog` (LAND_POSTHOG_*),
322+
// `.env.Land.Node` (LAND_NODE_*), `.env.Land.Extensions`
323+
// (LAND_USER_EXTENSIONS_DIR, LAND_EXTRA_EXTENSIONS_DIR,
324+
// LAND_DEV_EXTENSIONS_DIR, LAND_BUILTIN_EXTENSIONS_DIR,
325+
// LAND_AUTO_INSTALL_*, LAND_DISABLE_EXTENSIONS,
326+
// LAND_SKIP_BUILTIN_EXTENSIONS), and the kernel / mountain-only
327+
// gating flags (LAND_SPAWN_COCOON, LAND_ENABLE_WIND). Previously
328+
// only Product/Tier/Network were forwarded and Cocoon's PostHog
329+
// bridge fell back to the empty-string default - no telemetry
330+
// reached the EU project even when `.env.Land.PostHog` was present.
320331
for (Key, Value) in std::env::vars() {
321-
if Key.starts_with("Product") || Key.starts_with("Tier") || Key.starts_with("Network") {
332+
if Key.starts_with("Product")
333+
|| Key.starts_with("Tier")
334+
|| Key.starts_with("Network")
335+
|| Key.starts_with("LAND_")
336+
{
322337
EnvironmentVariables.insert(Key, Value);
323338
}
324339
}
325340

326-
// Atom I11: forward NODE_ENV / LAND_DEV_LOG / TAURI_ENV_DEBUG so
327-
// Cocoon's Bootstrap.ts stage2_configuration resolves real values.
328-
// Without this, env_clear() above leaves Cocoon seeing NodeEnv=
329-
// "production" / DevLog=<unset> / TauriDebug=false even on the
330-
// debug-electron profile - silently disabling dev-only logging,
331-
// stricter validation, and debug-only diagnostics in Cocoon.
332-
for Key in ["NODE_ENV", "LAND_DEV_LOG", "TAURI_ENV_DEBUG"] {
341+
// Atom I11: forward NODE_ENV / TAURI_ENV_DEBUG (LAND_DEV_LOG is
342+
// already covered by the `LAND_` prefix sweep above). Without this,
343+
// env_clear() leaves Cocoon seeing NodeEnv="production" /
344+
// TauriDebug=false even on the debug-electron profile - silently
345+
// disabling dev-only logging and debug-only diagnostics in Cocoon.
346+
for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
333347
if let Ok(Value) = std::env::var(Key) {
334348
EnvironmentVariables.insert(Key.to_string(), Value);
335349
}

0 commit comments

Comments
 (0)