@@ -37,6 +37,7 @@ const {
3737 focusedId,
3838 openWindow,
3939 closeWindow,
40+ closeAll,
4041 focusWindow,
4142 minimizeWindow,
4243 restoreWindow,
@@ -93,31 +94,65 @@ async function fetchWallpaper() {
9394// gate this.
9495const isConnecting = ref (true )
9596
96- // A WAMP session can connect fine even when the machine itself is offline;
97- // pinging deskconnd is what actually confirms it's reachable.
98- const isOffline = ref (false )
97+ // Set whenever there's no live session — the initial connect failed (e.g.
98+ // the machine is offline) or a previously-live session dropped mid-use
99+ // (session.onDisconnect() already fires for that today). Both cases get the
100+ // same full-screen treatment.
101+ const isDisconnected = ref (false )
102+
103+ // Apps stay open (blurred) behind the overlay while disconnected — only
104+ // cleared once the user actually navigates away (see the active-prop watch
105+ // below), so returning to a still-active view doesn't lose anything.
106+ function markDisconnected() {
107+ isDisconnected .value = true
108+ }
99109
100- async function ensureConnected() {
110+ // Acquires a session and confirms deskconnd is actually reachable (a WAMP
111+ // session can connect fine even when the machine itself is offline), then
112+ // arms the disconnect listener for the rest of its lifetime.
113+ async function connectSession(): Promise <boolean > {
101114 try {
102115 const session = await sessionCacheStore .acquire (props .realm )
103- if (! session ) {
104- isOffline .value = true
105- return
106- }
116+ if (! session ) return false
107117 await session .call (' io.xconn.deskconn.deskconnd.ping' )
118+ session .onDisconnect (async () => {
119+ markDisconnected ()
120+ })
121+ return true
108122 } catch {
109- isOffline .value = true
110- } finally {
111- isConnecting .value = false
123+ return false
112124 }
125+ }
126+
127+ // Used for both the initial connect and the "Reconnect" button — a failure
128+ // always lands back in the same disconnected overlay.
129+ async function connect() {
130+ isDisconnected .value = false
131+ isConnecting .value = true
132+ sessionCacheStore .clearUnreachable (props .realm )
133+ sessionCacheStore .invalidate (props .realm )
134+
135+ const connected = await connectSession ()
136+ isConnecting .value = false
137+ if (! connected ) markDisconnected ()
113138
114139 // Otherwise the stale session gets reused next time, stuck on whatever
115140 // fallback transport it originally connected with.
116- if (isOffline . value ) {
141+ if (! connected ) {
117142 sessionCacheStore .invalidate (props .realm )
118143 }
119144}
120145
146+ // Other panels (terminal, files, ...) notice a dead desktop the moment their
147+ // own next call fails, well before this session's onDisconnect would — so
148+ // mirror that signal here too instead of leaving the blur to lag behind.
149+ watch (
150+ () => sessionCacheStore .unreachableRealms .has (props .realm ),
151+ (unreachable ) => {
152+ if (unreachable ) markDisconnected ()
153+ },
154+ )
155+
121156const isMobile = ref (window .innerWidth < 768 )
122157function updateIsMobile() {
123158 isMobile .value = window .innerWidth < 768
@@ -411,13 +446,27 @@ function onActivateWindow(id: string) {
411446}
412447
413448function handleLaunch(appId : string ) {
414- if (isOffline .value ) return
449+ if (isDisconnected .value ) return
415450 const app = apps .find ((a ) => a .id === appId )
416451 if (app ) launchApp (app )
417452}
418453
454+ // App.vue keeps one DesktopSessionHost per realm mounted forever (v-show, not
455+ // v-if) so navigating to the dashboard and back never remounts this component
456+ // — onMounted's connect() never reruns on its own. Without this, a desktop
457+ // left disconnected just keeps showing the same stale blur/overlay every time
458+ // you come back to it instead of trying again.
419459watch (() => props .active , (active ) => {
420- if (active ) syncMaximizedBounds (maximizedContainerSize ())
460+ if (active ) {
461+ syncMaximizedBounds (maximizedContainerSize ())
462+ if (isDisconnected .value && ! isConnecting .value ) connect ()
463+ return
464+ }
465+
466+ // Leaving a disconnected desktop (Dashboard button, browser back, closing
467+ // the tab) discards its stale windows so the next visit starts fresh —
468+ // while still connected, navigating away/back is expected to keep them.
469+ if (isDisconnected .value ) closeAll ()
421470})
422471
423472watch (dockPosition , async () => {
@@ -430,7 +479,7 @@ let launcherBodyResizeObserver: ResizeObserver | null = null
430479
431480onMounted (() => {
432481 window .addEventListener (' resize' , updateIsMobile )
433- ensureConnected ()
482+ connect ()
434483 fetchWallpaper ()
435484 measureDockThickness ()
436485
@@ -451,15 +500,10 @@ onUnmounted(() => {
451500
452501<template >
453502 <div class =" launcher-wrapper fade-in-up" @contextmenu.prevent >
454- <div v-if =" isOffline" class =" offline-banner" >
455- <i class =" bi bi-wifi-off" ></i >
456- <span >{{ desktopName }} is offline — apps aren't available right now.</span >
457- </div >
458-
459503 <div
460504 ref =" launcherBodyRef"
461505 class =" launcher-body"
462- :class =" { 'has-wallpaper': !!wallpaperUrl, 'is-connecting': isConnecting }"
506+ :class =" { 'has-wallpaper': !!wallpaperUrl, 'is-connecting': isConnecting, 'is-disconnected': isDisconnected }"
463507 :style =" wallpaperUrl ? { backgroundImage: `url(${wallpaperUrl})`, backgroundSize: 'cover', backgroundPosition: 'center' } : {}"
464508 >
465509 <div class =" windows-layer" >
@@ -508,7 +552,7 @@ onUnmounted(() => {
508552 :windows =" windows "
509553 :focused-id =" focusedId "
510554 :position =" dockPosition "
511- :offline =" isOffline "
555+ :offline =" isDisconnected "
512556 @launch =" handleLaunch "
513557 @activate =" onActivateWindow "
514558 @close =" onCloseWindow "
@@ -522,6 +566,12 @@ onUnmounted(() => {
522566 <div class =" connecting-spinner" ></div >
523567 <p >Connecting to {{ desktopName }}…</p >
524568 </div >
569+
570+ <div v-if =" isDisconnected" class =" disconnected-overlay" >
571+ <i class =" bi bi-wifi-off" ></i >
572+ <p >Can't connect to {{ desktopName }}.</p >
573+ <button type =" button" class =" overlay-btn" @click =" close" >Dashboard</button >
574+ </div >
525575 </div >
526576</template >
527577
@@ -534,20 +584,6 @@ onUnmounted(() => {
534584 min-height : 0 ;
535585}
536586
537- .offline-banner {
538- display : flex ;
539- align-items : center ;
540- gap : 0.5rem ;
541- padding : 0.45rem 1rem ;
542- background : #fef3c7 ;
543- border-bottom : 1px solid #fbbf24 ;
544- color : #92400e ;
545- font-size : 0.8rem ;
546- font-weight : 500 ;
547- flex-shrink : 0 ;
548- z-index : 60 ;
549- }
550-
551587.launcher-body {
552588 position : relative ;
553589 flex : 1 ;
@@ -561,6 +597,11 @@ onUnmounted(() => {
561597 pointer-events : none ;
562598}
563599
600+ .launcher-body.is-disconnected {
601+ filter : grayscale (1 ) brightness (0.65 );
602+ pointer-events : none ;
603+ }
604+
564605.windows-layer {
565606 position : absolute ;
566607 inset : 0 ;
@@ -600,4 +641,40 @@ onUnmounted(() => {
600641 transform : rotate (360deg );
601642 }
602643}
644+
645+ .disconnected-overlay {
646+ position : absolute ;
647+ inset : 0 ;
648+ z-index : 50 ;
649+ display : flex ;
650+ flex-direction : column ;
651+ align-items : center ;
652+ justify-content : center ;
653+ gap : 0.75rem ;
654+ color : #f1f5f9 ;
655+ font-weight : 600 ;
656+ text-align : center ;
657+ padding : 1rem ;
658+ }
659+
660+ .disconnected-overlay i {
661+ font-size : 1.75rem ;
662+ }
663+
664+ .overlay-btn {
665+ margin-top : 0.25rem ;
666+ padding : 0.4rem 1.1rem ;
667+ border : 1px solid rgba (241 , 245 , 249 , 0.4 );
668+ border-radius : 0.4rem ;
669+ background : rgba (15 , 23 , 42 , 0.5 );
670+ color : #f1f5f9 ;
671+ font-weight : 600 ;
672+ font-size : 0.85rem ;
673+ cursor : pointer ;
674+ pointer-events : auto ;
675+ }
676+
677+ .overlay-btn :hover {
678+ background : rgba (15 , 23 , 42 , 0.75 );
679+ }
603680 </style >
0 commit comments