@@ -120,14 +120,23 @@ const _CelDispatchLog = (DomEvent: string, HasConsumer: boolean): void => {
120120/**
121121 * Retrieves the VS Code `IWorkbench` stored globally by Mountain.astro.
122122 * Returns null if the workbench has not loaded yet.
123+ *
124+ * Defensive: `window` itself can be undefined under SSR / Astro
125+ * pre-render evaluation; the global access path is wrapped to keep the
126+ * function safe to call from any module-eval context.
123127 */
124128function GetWorkbench ( ) : {
125129 commands : {
126130 executeCommand ( id : string , ...args : unknown [ ] ) : Promise < unknown > ;
127131 } ;
128132 env : { openUri ( target : unknown ) : Promise < boolean > } ;
129133} | null {
130- return ( window as any ) . __CEL_WORKBENCH__ ?? null ;
134+ try {
135+ if ( typeof window === "undefined" ) return null ;
136+ return ( window as any ) . __CEL_WORKBENCH__ ?? null ;
137+ } catch {
138+ return null ;
139+ }
131140}
132141
133142// Concrete workbench service handles written by the Output transform
@@ -233,6 +242,14 @@ interface CelServices {
233242}
234243
235244function GetServices ( ) : CelServices | null {
245+ // SSR safety: `window` is undefined during Astro's pre-render
246+ // pass. Returning `null` lets every caller keep its existing
247+ // `if (!Services) return;` early-return contract.
248+ try {
249+ if ( typeof window === "undefined" ) return null ;
250+ } catch {
251+ return null ;
252+ }
236253 return ( window as any ) . __CEL_SERVICES__ ?? null ;
237254}
238255
@@ -664,10 +681,45 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
664681 Channel : string ,
665682 Handler : ( Payload : any ) => void ,
666683 ) => {
667- const Unlisten = await listen < any > ( Channel , ( Event ) =>
668- Handler ( Event . payload ) ,
669- ) ;
670- Cleanups . push ( Unlisten ) ;
684+ // Centralized try/catch wrapper: a single bad handler must not
685+ // crash the Tauri listener loop, which would silently
686+ // disconnect future events on the same channel. Stock VS
687+ // Code's Emitter wraps each subscriber's call in a try/catch
688+ // for the same reason - one buggy listener should never
689+ // silence its peers.
690+ const SafeHandler = ( Payload : any ) : void => {
691+ try {
692+ Handler ( Payload ) ;
693+ } catch ( HandlerError ) {
694+ try {
695+ console . warn (
696+ `[SkyBridge] handler for ${ Channel } threw:` ,
697+ HandlerError ,
698+ ) ;
699+ } catch {
700+ /* console may be replaced */
701+ }
702+ }
703+ } ;
704+ try {
705+ const Unlisten = await listen < any > ( Channel , ( Event ) =>
706+ SafeHandler ( Event . payload ) ,
707+ ) ;
708+ Cleanups . push ( Unlisten ) ;
709+ } catch ( RegisterError ) {
710+ // Tauri's `listen()` can reject if the IPC bridge is torn
711+ // down mid-install (e.g. window closing during boot). Log
712+ // and continue - the rest of the bridge install must
713+ // still complete so other channels work.
714+ try {
715+ console . warn (
716+ `[SkyBridge] failed to register listener for ${ Channel } :` ,
717+ RegisterError ,
718+ ) ;
719+ } catch {
720+ /* console may be replaced */
721+ }
722+ }
671723 } ;
672724
673725 // Atom Q1: resolve UI requests via Mountain's `ResolveUIRequest` Tauri
@@ -2074,16 +2126,40 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
20742126 ] as const ;
20752127 for ( const Channel of FanOut ) {
20762128 await Register ( Channel , ( Payload : any ) => {
2077- const DomEvent = ChannelToDomEvent ( Channel ) ;
2078- document . dispatchEvent (
2079- new CustomEvent ( DomEvent , { detail : Payload } ) ,
2080- ) ;
2081- // `cel-dispatch` tag: surfaces whether this CustomEvent has
2082- // any consumer registered. Orphans (consumer-present=false)
2083- // are F1.1 indicators - Mountain's emit reaches the DOM
2084- // but nothing in the workbench listens, so the event
2085- // effectively vanishes.
2086- _CelDispatchLog ( DomEvent , _CelConsumers . has ( DomEvent ) ) ;
2129+ // Defensive: a single handler that throws (bad payload from
2130+ // upstream, dispatchEvent rejected by the DOM, etc.) must
2131+ // not stop the rest of the fan-out from running. Same
2132+ // philosophy as VS Code's `safeStringify` / event-emitter
2133+ // per-listener try/catch - one bad consumer never silences
2134+ // the others.
2135+ let DomEvent = "" ;
2136+ try {
2137+ DomEvent = ChannelToDomEvent ( Channel ) ;
2138+ document . dispatchEvent (
2139+ new CustomEvent ( DomEvent , { detail : Payload } ) ,
2140+ ) ;
2141+ } catch ( DispatchError ) {
2142+ try {
2143+ console . warn (
2144+ `[SkyBridge] FanOut dispatch failed for ${ Channel } :` ,
2145+ DispatchError ,
2146+ ) ;
2147+ } catch {
2148+ /* swallow - console may be replaced */
2149+ }
2150+ return ;
2151+ }
2152+ try {
2153+ // `cel-dispatch` tag: surfaces whether this CustomEvent
2154+ // has any consumer registered. Orphans
2155+ // (consumer-present=false) are F1.1 indicators -
2156+ // Mountain's emit reaches the DOM but nothing in the
2157+ // workbench listens, so the event effectively vanishes.
2158+ _CelDispatchLog ( DomEvent , _CelConsumers . has ( DomEvent ) ) ;
2159+ } catch {
2160+ /* dispatch-log failure must not propagate; the event
2161+ * itself already fired above */
2162+ }
20872163 } ) ;
20882164 }
20892165
@@ -2248,13 +2324,26 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
22482324 ? Response . items
22492325 : [ ] ;
22502326 const ParentHandle = Element ?. handle ?? "" ;
2251- const Items = RawItems . map ( ( Raw , Index ) =>
2252- ToTreeItem ( Raw , {
2253- ViewId,
2254- ParentHandle,
2255- Index,
2256- } ) ,
2257- ) ;
2327+ // Per-item try/catch so a single malformed tree node
2328+ // (extension-side serialisation glitch, missing
2329+ // `label`/`handle`) doesn't drop the entire panel
2330+ // children list. Stock VS Code's renderer skips bad
2331+ // items rather than failing the parent.
2332+ const Items : unknown [ ] = [ ] ;
2333+ for ( let Index = 0 ; Index < RawItems . length ; Index += 1 ) {
2334+ try {
2335+ Items . push (
2336+ ToTreeItem ( RawItems [ Index ] , {
2337+ ViewId,
2338+ ParentHandle,
2339+ Index,
2340+ } ) ,
2341+ ) ;
2342+ } catch {
2343+ /* skip the bad item; the rest of the children
2344+ * are still valid */
2345+ }
2346+ }
22582347 // Dual-emit: DOM CustomEvent for Sky-side observers
22592348 // (same shape as the workbench tree renderer sees so
22602349 // mirror panels don't need a second conversion).
@@ -2391,13 +2480,29 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
23912480 | undefined ;
23922481 const ViewId = Detail ?. viewId ?? "" ;
23932482 if ( ! ViewId ) return ;
2394- const Services = GetServices ( ) ;
2395- const TreeView = Services ?. TreeViewByViewId ?.( ViewId ) ;
2396- if ( TreeView ?. refresh ) {
2397- TreeView . refresh ( ) . catch ( ( ) => { } ) ;
2483+ // Defensive: `Services?.TreeViewByViewId?.()` itself could
2484+ // throw (Registry lookup with a freshly disposed view), and
2485+ // `TreeView.refresh()` may synchronously throw before
2486+ // returning a Promise (older xterm/tree shims). Wrap so a
2487+ // single failure doesn't crash the listener loop.
2488+ try {
2489+ const Services = GetServices ( ) ;
2490+ const TreeView = Services ?. TreeViewByViewId ?.( ViewId ) ;
2491+ if ( TreeView ?. refresh ) {
2492+ const RefreshResult = TreeView . refresh ( ) ;
2493+ if ( RefreshResult && typeof RefreshResult . catch === "function" ) {
2494+ RefreshResult . catch ( ( ) => { } ) ;
2495+ }
2496+ }
2497+ } catch {
2498+ /* swallow - already-disposed view / DI lookup race */
23982499 }
23992500 // Also re-prime the Sky observers.
2400- void ProvideChildren ( ViewId , undefined ) ;
2501+ try {
2502+ void ProvideChildren ( ViewId , undefined ) ;
2503+ } catch {
2504+ /* swallow */
2505+ }
24012506 } ) ;
24022507
24032508 // `cel:tree-view:dispose` - extension disposed its tree data
@@ -2410,10 +2515,16 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
24102515 | undefined ;
24112516 const ViewId = Detail ?. viewId ?? "" ;
24122517 if ( ! ViewId ) return ;
2413- const Services = GetServices ( ) ;
2414- const TreeView = Services ?. TreeViewByViewId ?.( ViewId ) ;
2415- if ( TreeView && TreeView . dataProvider !== undefined ) {
2416- TreeView . dataProvider = undefined ;
2518+ // Defensive: setter may throw if the workbench already
2519+ // torn down the view in a parallel disposal race.
2520+ try {
2521+ const Services = GetServices ( ) ;
2522+ const TreeView = Services ?. TreeViewByViewId ?.( ViewId ) ;
2523+ if ( TreeView && TreeView . dataProvider !== undefined ) {
2524+ TreeView . dataProvider = undefined ;
2525+ }
2526+ } catch {
2527+ /* view already disposed - nothing to clear */
24172528 }
24182529 } ) ;
24192530 }
@@ -2701,12 +2812,35 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
27012812 const Handle = Args [ 0 ] ?? Payload ?. handle ;
27022813 const ViewId : string = String ( Args [ 1 ] ?? Payload ?. viewId ?? "" ) ;
27032814 if ( ! ViewId ) return ;
2704- WebviewViewResolvers . set ( ViewId , Number ( Handle ) ) ;
2705- document . dispatchEvent (
2706- new CustomEvent ( "cel:webview:registerView" , {
2707- detail : { handle : Handle , viewId : ViewId , payload : Payload } ,
2708- } ) ,
2709- ) ;
2815+ // Defensive: a malformed payload (Mountain emit shape drift,
2816+ // missing handle, etc.) shouldn't kill the rest of the
2817+ // listener pipeline. Track + DOM-dispatch are best-effort;
2818+ // the WebviewViewService.register call below is what actually
2819+ // makes the panel work, so isolate failures so one doesn't
2820+ // cascade into the other.
2821+ try {
2822+ WebviewViewResolvers . set ( ViewId , Number ( Handle ) ) ;
2823+ } catch {
2824+ /* Map.set on a non-string viewId is unreachable since we
2825+ * String()-coerced above, but keep the guard so a future
2826+ * payload-shape change can't poison the registry */
2827+ }
2828+ try {
2829+ document . dispatchEvent (
2830+ new CustomEvent ( "cel:webview:registerView" , {
2831+ detail : { handle : Handle , viewId : ViewId , payload : Payload } ,
2832+ } ) ,
2833+ ) ;
2834+ } catch ( DispatchError ) {
2835+ try {
2836+ console . warn (
2837+ `[SkyBridge] webview/registerView CustomEvent dispatch failed for ${ ViewId } :` ,
2838+ DispatchError ,
2839+ ) ;
2840+ } catch {
2841+ /* console may be replaced */
2842+ }
2843+ }
27102844 // Per-fire trace so the SkyEmit -> Sky-listener bridge is
27112845 // observable in Mountain.dev.log. The listener used to silently
27122846 // `return` when `Services?.WebviewViews?.register` was missing,
@@ -2829,8 +2963,29 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
28292963 }
28302964 } ,
28312965 } ) ;
2832- } catch ( _e ) {
2833- /* swallow - workbench DI not yet resolved */
2966+ } catch ( RegisterError ) {
2967+ // `IWebviewViewService.register` throws on duplicate viewId -
2968+ // stock VS Code's `webviewViewService.ts:108` does
2969+ // `throw new Error("View resolver already registered for ...")`
2970+ // when a viewId is registered twice. That happens when the
2971+ // extension host re-registers after a hot-reload or when our
2972+ // SkyBridge reentrancy guard didn't engage in time. Swallow
2973+ // the dup-error specifically (the existing resolver is
2974+ // already serving the view); log anything else so we can
2975+ // triage real failures.
2976+ try {
2977+ const Message = ( RegisterError as any ) ?. message ?? String (
2978+ RegisterError ,
2979+ ) ;
2980+ if ( ! String ( Message ) . includes ( "already registered" ) ) {
2981+ console . warn (
2982+ `[SkyBridge] WebviewViews.register failed for ${ ViewId } :` ,
2983+ RegisterError ,
2984+ ) ;
2985+ }
2986+ } catch {
2987+ /* console may be replaced */
2988+ }
28342989 }
28352990 } ) ;
28362991
0 commit comments