Skip to content

Commit 876ea91

Browse files
fix(Sky): Add reentrancy guard to InstallSkyBridge and phase advancement
The InstallSkyBridge function lacked reentrancy protection, causing duplicate Tauri listen() registrations on Astro view-transitions, Tauri webview reloads, and dev HMR. Each Mountain emit fired handlers N times, resulting in duplicated tree views, markers, and webview renders—the "workbench loading twice" / "purple overlays" bug. Add a module-level guard (_SkyBridgeInstalled, _SkyBridgeInstallPromise) so multiple calls to InstallSkyBridge only execute once. Also add the phase advance script to Layout.astro that fires BEFORE the workbench import, mirroring Mountain.astro to prevent the 8s/23s fallback timers from leaving the workbench in a half-restored render state. Finally, add a +5s view-registry snapshot diagnostic to probe extension-point registration status. Additionally, update astro.config.ts: set output="static", disable clientPrerender and contentIntellisense, and add queuedRendering config. This restores responsive UI during startup and eliminates the double-render flicker.
1 parent d574527 commit 876ea91

4 files changed

Lines changed: 123 additions & 39 deletions

File tree

Source/Function/Markup/Base.astro

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,6 @@ import Meta from "../Meta.astro";
55
<!doctype html>
66
<html lang="en" class="no-js" dir="ltr">
77
<head>
8-
<!-- Early paint colour. Without this the WKWebView renders its
9-
default white → VS Code's theme CSS → native-titlebar repaint
10-
all in ~200 ms, visible as a purple/fuchsia/white flash on
11-
every Tauri `Window.navigate()` reload (reported as
12-
"overlaying purple windows" + stuttering). Matching VS Code
13-
Dark+ (`#1e1e1e` editor bg, `#d4d4d4` editor fg) keeps the
14-
first paint inside the app's dark band so the flash
15-
disappears. Kept inline so it applies before any external
16-
CSS loads. -->
17-
<style is:inline>
18-
html,
19-
body {
20-
background: #1e1e1e;
21-
color: #d4d4d4;
22-
margin: 0;
23-
}
24-
</style>
25-
268
<script>
279
document.documentElement.classList.remove("no-js");
2810

Source/Function/SkyBridge.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,41 @@ function GetServices(): CelServices | null {
316316
// but isn't the real `WebviewViewService`) still surfaces.
317317
const RegisterShape = `WebviewViews.register=${typeof (S["WebviewViews"] as any)?.register} Markers.changeOne=${typeof (S["Markers"] as any)?.changeOne}`;
318318
ToMountain("cel-services", RegisterShape);
319+
// View-registry snapshot. The Output transform's
320+
// `ViewRegistrySnapshot()` accessor (added in
321+
// `ExposeWorkbenchAccessor.ts`) walks the workbench's
322+
// `IViewContainersRegistry` and `IViewsRegistry`, returning
323+
// counts + sample IDs. Logged at +5s so the extension-points
324+
// pipeline has time to flush. If `containers` is still tiny
325+
// (only built-ins like `workbench.view.explorer`) and no
326+
// extension-contributed IDs (`roo-cline`, `claude-vscode`,
327+
// `gitlens.views.welcome`, ...) appear, the issue is that
328+
// extension manifests aren't reaching
329+
// `viewsContainersExtensionPoint.setHandler` - meaning the
330+
// workbench's `IExtensionService` never received those
331+
// extensions' descriptions through the `_registerExtensions`
332+
// path. Activity bar stays empty, panels can't open.
333+
setTimeout(() => {
334+
try {
335+
const Snapshot = (S as any)?.ViewRegistrySnapshot?.();
336+
if (!Snapshot) {
337+
ToMountain(
338+
"view-registry",
339+
"snapshot accessor missing on __CEL_SERVICES__",
340+
);
341+
return;
342+
}
343+
ToMountain(
344+
"view-registry",
345+
`containers=${Snapshot.containers} views=${Snapshot.views} containerSample=${(Snapshot.containerSample ?? []).join(",")} viewSample=${(Snapshot.viewSample ?? []).join(",")}`,
346+
);
347+
} catch (Error) {
348+
ToMountain(
349+
"view-registry",
350+
`probe failed: ${(Error as Error)?.message ?? String(Error)}`,
351+
);
352+
}
353+
}, 5000);
319354
};
320355
if (typeof window !== "undefined") {
321356
// If services already ready by the time this module loads, probe
@@ -592,8 +627,38 @@ function ShowNotification(
592627
/**
593628
* Install all `sky://` event listeners. Call this AFTER the VS Code
594629
* workbench has loaded (so `__CEL_WORKBENCH__` is available).
630+
*
631+
* **Reentrancy:** the function is idempotent. Multiple calls (Astro
632+
* view-transition, Tauri webview reload, dev HMR re-import) only attach
633+
* the listener set once. Without this guard every double-call doubled
634+
* the Tauri `listen()` registrations, so each Mountain emit fired every
635+
* `sky://*` handler N times - rendering the same tree view twice,
636+
* inserting the same marker twice, painting the same webview twice,
637+
* etc. That looked exactly like "the workbench is loading twice" /
638+
* "purple overlays / panels not rendering properly" in the renderer.
595639
*/
640+
let _SkyBridgeInstalled = false;
641+
let _SkyBridgeInstallPromise: Promise<void> | null = null;
642+
596643
export async function InstallSkyBridge(): Promise<void> {
644+
if (_SkyBridgeInstalled) {
645+
return;
646+
}
647+
if (_SkyBridgeInstallPromise) {
648+
return _SkyBridgeInstallPromise;
649+
}
650+
_SkyBridgeInstallPromise = (async () => {
651+
try {
652+
await _InstallSkyBridgeOnce();
653+
_SkyBridgeInstalled = true;
654+
} finally {
655+
_SkyBridgeInstallPromise = null;
656+
}
657+
})();
658+
return _SkyBridgeInstallPromise;
659+
}
660+
661+
async function _InstallSkyBridgeOnce(): Promise<void> {
597662
const Cleanups: Array<() => void> = [];
598663
const Register = async (
599664
Channel: string,

Source/Workbench/Bundled/Electron/Layout.astro

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,39 @@ import TelemetryBridge from "../../TelemetryBridge.astro";
2626
<NLS />
2727
<TelemetryBridge />
2828

29+
<!-- Phase advance script - fires BEFORE the workbench import so it
30+
is not blocked by the main workbench script. Mirrors the
31+
equivalent block in `Mountain.astro:30` so the bundled Electron
32+
route doesn't hang at phase 2 / 3 waiting for Mountain's 8s /
33+
23s fallback timers (visible in the log as
34+
`[Lifecycle] [Fallback] Sky did not advance to Restored within
35+
8s ...`). The fallback delay leaves the workbench in a
36+
half-restored render state - editor state-restoration gated on
37+
phase=Restored is deferred, "Eventually" work is deferred -
38+
which surfaces as sluggish UI / heavy-on-the-eye flicker
39+
during the first 8-23 seconds. -->
40+
<script type="module">
41+
const PhaseInvoke =
42+
globalThis.__TAURI_INTERNALS__?.invoke ??
43+
globalThis.__TAURI__?.invoke ??
44+
globalThis.__TAURI__?.core?.invoke ??
45+
null;
46+
if (PhaseInvoke) {
47+
const Advance = (PhaseNumber) =>
48+
PhaseInvoke("MountainIPCInvoke", {
49+
method: "lifecycle:advancePhase",
50+
params: [PhaseNumber],
51+
}).catch(() => {});
52+
// Phase 3 = Restored: workbench DOM is attached, first
53+
// paint imminent. Mountain rejects same-/backwards-phase
54+
// advances, so racing the workbench import is safe.
55+
Advance(3);
56+
// Phase 4 = Eventually: long-tail background work can
57+
// start. Delayed so 3 → 4 stays monotonic.
58+
setTimeout(() => Advance(4), 1500);
59+
}
60+
</script>
61+
2962
<!-- Sequential: Preload -> Polyfills -> Bootstrap -> Entry (bundled workbench) -> SkyBridge -->
3063
<script type="module">
3164
await import("../../Electron/WindPreload.js");

astro.config.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ import { External, Host, Link, On } from "./Source/Function/Debug";
6363
// empty and the `vs/**` external rules below stay in effect - existing
6464
// builds are byte-for-byte identical.
6565
// -----------------------------------------------------------------------------
66-
const BundledVariants = ["electron", "browser", "sessions", "workbench"] as const;
66+
const BundledVariants = [
67+
"electron",
68+
"browser",
69+
"sessions",
70+
"workbench",
71+
] as const;
6772
type BundledVariant = (typeof BundledVariants)[number];
6873

6974
const BundledList = (process.env["Pack"] ?? "")
@@ -616,9 +621,7 @@ export default defineConfig({
616621
// Scanner observes the same flag (Atom J3) and returns early
617622
// for the built-in fallback paths, so the runtime matches the
618623
// zero-on-disk state.
619-
if (
620-
process.env["Skip"] === "true"
621-
) {
624+
if (process.env["Skip"] === "true") {
622625
console.log(
623626
"[CopyVSCode] Step 13: Skip=true - skipping built-in extension copy",
624627
);
@@ -645,8 +648,7 @@ export default defineConfig({
645648
// `Install=false` for CI
646649
// runs that pre-populate the cache.
647650
const AutoInstallDeps =
648-
process.env["Install"] !==
649-
"false";
651+
process.env["Install"] !== "false";
650652
const InstallLog: Array<{
651653
Name: string;
652654
Installed: number;
@@ -739,11 +741,7 @@ export default defineConfig({
739741
// exporting the flag in the shell or
740742
// `.env.Land.Local`. Preserve the call-to-action at
741743
// the end so the opt-in output is self-explanatory.
742-
if (
743-
process.env[
744-
"Warn"
745-
] === "true"
746-
) {
744+
if (process.env["Warn"] === "true") {
747745
for (const Warning of BundleWarnings) {
748746
console.warn(
749747
`[CopyVSCode] Step 13: bundle warning - ${Warning}`,
@@ -807,12 +805,22 @@ export default defineConfig({
807805
},
808806
],
809807

808+
output: "static",
809+
810810
experimental: {
811-
clientPrerender: true,
811+
clientPrerender: false,
812812

813-
contentIntellisense: true,
813+
contentIntellisense: false,
814814

815815
rustCompiler: true,
816+
817+
queuedRendering: {
818+
enabled: false,
819+
820+
contentCache: false,
821+
822+
poolSize: 1000,
823+
},
816824
},
817825

818826
vite: {
@@ -853,11 +861,9 @@ export default defineConfig({
853861
"import.meta.env.Report": JSON.stringify(
854862
process.env["Report"] ?? "true",
855863
),
856-
"import.meta.env.Throttle":
857-
JSON.stringify(
858-
process.env["Throttle"] ??
859-
"5",
860-
),
864+
"import.meta.env.Throttle": JSON.stringify(
865+
process.env["Throttle"] ?? "5",
866+
),
861867
"import.meta.env.Buffer": JSON.stringify(
862868
process.env["Buffer"] ?? "3000",
863869
),
@@ -870,9 +876,7 @@ export default defineConfig({
870876
"import.meta.env.Ask": JSON.stringify(
871877
process.env["Ask"] ?? "false",
872878
),
873-
"import.meta.env.Brand": JSON.stringify(
874-
process.env["Brand"] ?? "",
875-
),
879+
"import.meta.env.Brand": JSON.stringify(process.env["Brand"] ?? ""),
876880
},
877881

878882
build: {

0 commit comments

Comments
 (0)