@@ -16,9 +16,11 @@ import { isPeerDeviceModeActive } from '@/infrastructure/peer-device/peerModeFla
1616import {
1717 getActiveSurfaceScope ,
1818 isSurfaceChangedError ,
19+ onSurfaceActivated ,
1920 runInSurfaceScope ,
2021 type SurfaceScope ,
2122} from '@/infrastructure/peer-device/deviceSurface' ;
23+ import { classifyPeerHostRefusal } from '@/infrastructure/peer-device/peerHostRefusal' ;
2224import { useI18n } from '@/infrastructure/i18n' ;
2325import { confirmWarning } from '@/infrastructure/confirm-dialog' ;
2426import {
@@ -328,6 +330,40 @@ export const SSHRemoteProvider: React.FC<SSHRemoteProviderProps> = ({ children }
328330 setWorkspaceStatuses ( prev => ( { ...prev , [ connId ] : st } ) ) ;
329331 } , [ ] ) ;
330332
333+ /**
334+ * Peer Device Mode: the peer owns every SSH transport, so ask it instead of
335+ * leaving its remote workspaces without a status. A host that does not report
336+ * SSH transport state (a CLI peer) still owns the workspace it opened, so it
337+ * keeps the mirrored `connected` state; only an unreachable peer is an error.
338+ */
339+ const peerStatusProbesRef = useRef ( new Map < string , number > ( ) ) ;
340+ const reconcilePeerRemoteStatuses = useCallback ( ( force : boolean ) => {
341+ if ( ! mountedRef . current || ! isPeerDeviceModeActive ( ) ) return ;
342+ const scope = getActiveSurfaceScope ( ) ;
343+ const probes = peerStatusProbesRef . current ;
344+ for ( const workspace of workspaceManager . getState ( ) . openedWorkspaces . values ( ) ) {
345+ const connId = workspace . workspaceKind === WorkspaceKind . Remote ? workspace . connectionId ?. trim ( ) : '' ;
346+ if ( ! connId || probes . get ( connId ) === scope . epoch ) continue ;
347+ if ( ! force && workspaceStatusesRef . current [ connId ] ) continue ;
348+ probes . set ( connId , scope . epoch ) ;
349+ void runInSurfaceScope ( scope , 'probe peer SSH status' , ( ) => sshApi . isConnected ( connId ) )
350+ . then (
351+ ( connected ) : ConnectionStatus => ( connected ? 'connected' : 'disconnected' ) ,
352+ ( error ) : ConnectionStatus | null => {
353+ if ( isSurfaceChangedError ( error ) ) return null ;
354+ if ( classifyPeerHostRefusal ( error ) ) return 'connected' ;
355+ log . warn ( 'Failed to read peer SSH connection status' , { connectionId : connId , error } ) ;
356+ return 'error' ;
357+ } ,
358+ )
359+ . then ( status => {
360+ if ( probes . get ( connId ) === scope . epoch ) probes . delete ( connId ) ;
361+ if ( ! status || ! mountedRef . current || ! scope . isCurrent ( ) || ! isPeerDeviceModeActive ( ) ) return ;
362+ setWorkspaceStatuses ( prev => ( prev [ connId ] === status ? prev : { ...prev , [ connId ] : status } ) ) ;
363+ } ) ;
364+ }
365+ } , [ ] ) ;
366+
331367 const reportRemoteWorkspaceReconnectFailure = useCallback ( ( workspace : RemoteWorkspace ) => {
332368 if ( isPeerDeviceModeActive ( ) ) {
333369 return ;
@@ -969,6 +1005,7 @@ export const SSHRemoteProvider: React.FC<SSHRemoteProviderProps> = ({ children }
9691005 }
9701006 workspaceStatusTimeouts . current . clear ( ) ;
9711007 log . info ( 'Paused SSH heartbeat while peer device mode is active' ) ;
1008+ reconcilePeerRemoteStatuses ( true ) ;
9721009 } else if ( detail ?. active === false ) {
9731010 if ( showFileBrowserRef . current && connectionIdRef . current && isConnectedRef . current
9741011 && usedConnectionIds ( ) . has ( connectionIdRef . current ) ) {
@@ -979,7 +1016,11 @@ export const SSHRemoteProvider: React.FC<SSHRemoteProviderProps> = ({ children }
9791016 } ;
9801017 window . addEventListener ( 'peer-mode:changed' , onPeerModeChanged ) ;
9811018 return ( ) => window . removeEventListener ( 'peer-mode:changed' , onPeerModeChanged ) ;
982- } , [ stopHeartbeat , usedConnectionIds ] ) ;
1019+ } , [ reconcilePeerRemoteStatuses , stopHeartbeat , usedConnectionIds ] ) ;
1020+
1021+ // Statuses are keyed by connection ID, which another device may reuse: a
1022+ // newly rendered peer surface reads every one of its connections again.
1023+ useEffect ( ( ) => onSurfaceActivated ( ( ) => reconcilePeerRemoteStatuses ( true ) ) , [ reconcilePeerRemoteStatuses ] ) ;
9831024
9841025 useEffect ( ( ) => {
9851026 return workspaceManager . addEventListener ( event => {
@@ -988,6 +1029,15 @@ export const SSHRemoteProvider: React.FC<SSHRemoteProviderProps> = ({ children }
9881029 pruneHeartbeats ( ) ;
9891030 return ;
9901031 }
1032+ if ( isPeerDeviceModeActive ( ) ) {
1033+ // Peer Device Mode: the peer owns the SSH connection lifecycle. Never
1034+ // run the controller-side reconnect/timeout removal path — those
1035+ // invokes route to the peer and would delete the peer's workspace.
1036+ // A hydrated list may arrive without per-workspace events, so every
1037+ // change reads the status of any connection that has none yet.
1038+ reconcilePeerRemoteStatuses ( false ) ;
1039+ return ;
1040+ }
9911041 const workspace =
9921042 'workspace' in event && event . workspace ?. workspaceKind === WorkspaceKind . Remote
9931043 ? event . workspace
@@ -999,23 +1049,13 @@ export const SSHRemoteProvider: React.FC<SSHRemoteProviderProps> = ({ children }
9991049 if ( ! connId ) {
10001050 return ;
10011051 }
1002- if ( isPeerDeviceModeActive ( ) ) {
1003- // Peer Device Mode: the peer owns the SSH connection lifecycle. Mirror
1004- // its opened remote workspaces as connected and never run the
1005- // controller-side reconnect/timeout removal path — those invokes route
1006- // to the peer and would delete the peer's workspace.
1007- if ( workspaceStatusesRef . current [ connId ] !== 'connected' ) {
1008- setWorkspaceStatus ( connId , 'connected' ) ;
1009- }
1010- return ;
1011- }
10121052 if ( ! workspaceStatusesRef . current [ connId ] ) {
10131053 setWorkspaceStatus ( connId , 'connecting' ) ;
10141054 }
10151055 const metadataOnly = event . type === 'workspace:updated' || event . type === 'workspace:primary-assistant-changed' ;
10161056 void checkRemoteWorkspaceRef . current ( ! metadataOnly ) ;
10171057 } ) ;
1018- } , [ pruneHeartbeats , restoredScope , setWorkspaceStatus ] ) ;
1058+ } , [ pruneHeartbeats , reconcilePeerRemoteStatuses , restoredScope , setWorkspaceStatus ] ) ;
10191059
10201060 const connect = useCallback ( async (
10211061 _connId : string ,
0 commit comments