Skip to content

Commit 96d6019

Browse files
feat(Sky): Subscribe to Mountain extension events for live sidebar updates
Add the ExtensionChangeSubscriber module that listens to Mountain's `sky://extensions/installed` and `sky://extensions/uninstalled` events and forwards them to the VS Code workbench's ExtensionEnablementService. This enables the sidebar to refresh live after a VSIX install or uninstall — no workbench reload required. The subscriber is imported and started in Bootstrap.ts as Wave 7 of the Electron boot sequence, only when LAND_ENABLE_WIND !== "false". It uses Effect.runFork for fire-and-forget semantics so the boot path never blocks on a stream that by definition never completes. Missing refresh hooks are handled gracefully with best-effort logging. This implements the K2/K3 milestone for live extension state synchronization between Mountain and the Wind/Sky frontend.
1 parent 4be63e1 commit 96d6019

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

Source/Workbench/Electron/Bootstrap.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ if (import.meta.env["LAND_ENABLE_WIND"] === "false") {
5959
"land:bootstrap:start",
6060
"land:bootstrap:done",
6161
);
62+
63+
// Wave 7: subscribe to Mountain's extension install/uninstall events
64+
// so the sidebar refreshes live after a VSIX install (K2/K3) — no
65+
// workbench reload required. Fire-and-forget; the subscriber logs
66+
// its own performance.mark on start / error / skipped states.
67+
const { default: StartExtensionSubscriber } = await import(
68+
"./ExtensionChangeSubscriber.js"
69+
);
70+
void StartExtensionSubscriber();
6271
} catch {
6372
performance.mark("land:bootstrap:error");
6473
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @module Workbench/Electron/ExtensionChangeSubscriber
3+
* @description
4+
* Subscribes to Mountain's `sky://extensions/installed` + `…/uninstalled`
5+
* events and forwards each change to the VS Code workbench's extension
6+
* registry so the sidebar refreshes live after a VSIX install/uninstall —
7+
* no workbench reload required.
8+
*
9+
* Design note: Wind exposes the merged typed stream as
10+
* `Effect/Extensions/ChangeStream.ts` (wave 6). This subscriber is a
11+
* fire-and-forget adapter that:
12+
*
13+
* 1. Builds the stream from Wind's IPC layer.
14+
* 2. Runs it forever on an Effect.runFork so the boot path never
15+
* blocks on a stream that by definition never completes.
16+
* 3. On each item, logs a performance.mark + attempts to call the
17+
* bundled workbench's `ExtensionEnablementService` refresh hook
18+
* when available. The hook isn't always present (browser / kernel
19+
* profiles omit it); when missing we just log and move on.
20+
*
21+
* No-op when `LAND_ENABLE_WIND === "false"` — if the Wind runtime is
22+
* not loaded there's no IPC to subscribe to.
23+
*/
24+
25+
interface ExtensionChangeBase {
26+
readonly Kind: "Installed" | "Uninstalled";
27+
readonly Identifier: string;
28+
}
29+
30+
interface WorkbenchRefreshHost {
31+
readonly _servicesAccess?: {
32+
readonly get?: (
33+
Key: unknown,
34+
) => { readonly refresh?: () => void | Promise<void> } | undefined;
35+
};
36+
}
37+
38+
const TryRefreshWorkbench = (Change: ExtensionChangeBase): void => {
39+
performance.mark(
40+
`land:extensions:${Change.Kind.toLowerCase()}:${Change.Identifier}`,
41+
);
42+
43+
const Host = (
44+
globalThis as unknown as { readonly __landWorkbench?: WorkbenchRefreshHost }
45+
).__landWorkbench;
46+
47+
const RefreshFn = Host?._servicesAccess?.get?.(
48+
"extensionEnablementService",
49+
)?.refresh;
50+
51+
if (typeof RefreshFn === "function") {
52+
try {
53+
void RefreshFn();
54+
} catch {
55+
// Best-effort only — the workbench will self-heal on next render.
56+
}
57+
}
58+
};
59+
60+
export default async (): Promise<void> => {
61+
if (import.meta.env["LAND_ENABLE_WIND"] === "false") {
62+
performance.mark("land:extensions:subscriber:skipped-wind-disabled");
63+
return;
64+
}
65+
66+
try {
67+
const Stream = (await import(
68+
"@codeeditorland/wind/Target/Effect/Extensions/ChangeStream"
69+
)) as {
70+
readonly default: unknown;
71+
};
72+
73+
const { Effect, Stream: EffectStream } = await import("effect");
74+
75+
const Subscription = Effect.gen(function* () {
76+
const Source = (yield* Stream.default as never) as unknown as {
77+
readonly pipe: (
78+
..._: unknown[]
79+
) => unknown;
80+
};
81+
82+
yield* EffectStream.runForEach(Source, (Change) =>
83+
Effect.sync(() =>
84+
TryRefreshWorkbench(Change as ExtensionChangeBase),
85+
),
86+
);
87+
});
88+
89+
// Fire-and-forget — the stream runs until the webview unloads.
90+
Effect.runFork(Subscription as never);
91+
92+
performance.mark("land:extensions:subscriber:started");
93+
} catch {
94+
performance.mark("land:extensions:subscriber:error");
95+
}
96+
};

0 commit comments

Comments
 (0)