Skip to content

Commit 1bd0a28

Browse files
committed
fix(remote-connect): unify relay connections and show live clients
1 parent 45fb4ef commit 1bd0a28

17 files changed

Lines changed: 667 additions & 140 deletions

File tree

‎src/apps/desktop/src/api/remote_connect_api.rs‎

Lines changed: 189 additions & 35 deletions
Large diffs are not rendered by default.

‎src/crates/services/services-integrations/src/remote_connect.rs‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,13 @@ pub enum RemotePermissionMode {
22682268
FullAccess,
22692269
}
22702270

2271+
/// Display-only identity for a live browser connection, never an authorization identity.
2272+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2273+
pub struct RemoteControlClient {
2274+
pub id: String,
2275+
pub name: String,
2276+
}
2277+
22712278
/// Commands that remote clients can send to the desktop runtime.
22722279
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22732280
#[serde(tag = "cmd", rename_all = "snake_case")]
@@ -2428,7 +2435,10 @@ pub enum RemoteCommand {
24282435
device_name: String,
24292436
request_id: String,
24302437
},
2431-
Ping,
2438+
Ping {
2439+
#[serde(default, skip_serializing_if = "Option::is_none")]
2440+
client: Option<RemoteControlClient>,
2441+
},
24322442

24332443
// ── Device-to-device distributed control ──────────────────────────────
24342444
//
@@ -2728,7 +2738,7 @@ where
27282738
H: RemoteCommandRuntimeHost + ?Sized,
27292739
{
27302740
match command {
2731-
RemoteCommand::Ping => RemoteResponse::Pong,
2741+
RemoteCommand::Ping { .. } => RemoteResponse::Pong,
27322742

27332743
RemoteCommand::GetWorkspaceInfo
27342744
| RemoteCommand::ListRecentWorkspaces

‎src/crates/services/services-integrations/tests/remote_connect_contracts.rs‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,7 +1183,7 @@ async fn remote_connect_command_owner_preserves_cancel_and_group_routing() {
11831183
assert_eq!(
11841184
handle_remote_command(
11851185
&host,
1186-
&RemoteCommand::Ping,
1186+
&RemoteCommand::Ping { client: None },
11871187
RemoteConnectSubmissionSource::Relay
11881188
)
11891189
.await,
@@ -1786,7 +1786,8 @@ async fn remote_connect_file_command_handler_owns_owner_flow_and_uses_host_root(
17861786
&[Some("session-1".to_string())]
17871787
);
17881788

1789-
let error = handle_remote_workspace_file_command(&host, &RemoteCommand::Ping).await;
1789+
let error =
1790+
handle_remote_workspace_file_command(&host, &RemoteCommand::Ping { client: None }).await;
17901791
assert_eq!(
17911792
error,
17921793
RemoteResponse::Error {
@@ -3151,3 +3152,26 @@ fn remote_connect_tool_preview_slimming_keeps_short_fields_and_drops_large_strin
31513152

31523153
assert!(make_slim_tool_params(&serde_json::json!(42)).is_none());
31533154
}
3155+
3156+
#[test]
3157+
fn control_ping_accepts_legacy_and_additive_client_identity() {
3158+
use openbitfun_services_integrations::remote_connect::RemoteCommand;
3159+
let legacy = serde_json::json!({ "cmd": "ping" });
3160+
let old: RemoteCommand = serde_json::from_value(legacy.clone()).unwrap();
3161+
assert_eq!(old, RemoteCommand::Ping { client: None });
3162+
assert_eq!(serde_json::to_value(old).unwrap(), legacy);
3163+
let current =
3164+
serde_json::json!({ "cmd": "ping", "client": { "id": "page-1", "name": "Safari · iOS" } });
3165+
let decoded: RemoteCommand = serde_json::from_value(current.clone()).unwrap();
3166+
assert_eq!(serde_json::to_value(decoded).unwrap(), current);
3167+
// Previous hosts use an internally tagged unit variant and ignore additive fields.
3168+
#[derive(serde::Deserialize)]
3169+
#[serde(tag = "cmd", rename_all = "snake_case")]
3170+
enum LegacyCommand {
3171+
Ping,
3172+
}
3173+
assert!(matches!(
3174+
serde_json::from_value::<LegacyCommand>(current).unwrap(),
3175+
LegacyCommand::Ping
3176+
));
3177+
}

‎src/mobile-web/src/services/RemoteSessionManager.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
RelayHttpClient,
1313
type ControlTargetSnapshot,
1414
} from './RelayHttpClient';
15+
import { getControlClientIdentity } from './controlClientIdentity';
1516

1617
export class RemoteControlTargetChangedError extends Error {
1718
constructor() {
@@ -571,7 +572,7 @@ export class RemoteSessionManager {
571572
}
572573

573574
async ping(): Promise<void> {
574-
await this.request({ cmd: 'ping' });
575+
await this.request({ cmd: 'ping', client: getControlClientIdentity() });
575576
}
576577

577578
/**
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
let identity: { id: string; name: string } | undefined;
2+
3+
/** One identity per browser page, shared by managers across target/reconnect changes. */
4+
export function getControlClientIdentity(): { id: string; name: string } {
5+
if (identity) return identity;
6+
const bytes = crypto.getRandomValues(new Uint8Array(16));
7+
const id = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
8+
const ua = navigator.userAgent;
9+
const browser = /Edg\//.test(ua) ? 'Edge'
10+
: /Firefox\/|FxiOS\//.test(ua) ? 'Firefox'
11+
: /Chrome\/|CriOS\//.test(ua) ? 'Chrome'
12+
: /Safari\//.test(ua) ? 'Safari' : 'Browser';
13+
const platform = /iPhone|iPad|iPod/.test(ua) ? 'iOS'
14+
: /Android/.test(ua) ? 'Android'
15+
: /Windows/.test(ua) ? 'Windows'
16+
: /Macintosh/.test(ua) ? 'macOS'
17+
: /Linux/.test(ua) ? 'Linux' : '';
18+
identity = { id, name: [browser, platform].filter(Boolean).join(' · ') };
19+
return identity;
20+
}

‎src/web-ui/src/app/components/NavPanel/NavPanel.scss‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,8 +1472,8 @@ $_section-header-height: 22px;
14721472
line-height: var(--openbitfun-type-modifier-leading-dense-line-height);
14731473
}
14741474

1475-
// Attached remote parties are shown once per device kind. Message-app marks
1476-
// stay unframed and inherit the theme foreground; connection state is already
1475+
// Attached remote parties are shown once per device kind with unframed icons.
1476+
// Message-app marks inherit the theme foreground; connection state is already
14771477
// communicated by the device overview rather than by recoloring the brand.
14781478
.openbitfun-nav-panel__footer-device-status-attached {
14791479
display: flex;
@@ -1492,14 +1492,9 @@ $_section-header-height: 22px;
14921492
justify-content: center;
14931493
gap: 1px;
14941494
box-sizing: border-box;
1495-
border: 1px solid var(--openbitfun-color-border-subtle);
1496-
border-radius: var(--openbitfun-radius-sm);
1497-
background: var(--openbitfun-color-surface-subtle);
14981495
line-height: var(--openbitfun-type-modifier-leading-none-line-height);
14991496

15001497
&[data-openbitfun-device-kind='message-app'] {
1501-
border: 0;
1502-
background: transparent;
15031498
color: var(--openbitfun-color-content-primary);
15041499
}
15051500
}

‎src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.scss‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,3 +1190,85 @@
11901190
transition: none;
11911191
}
11921192
}
1193+
1194+
.openbitfun-remote-connect__relay-address {
1195+
padding: var(--openbitfun-space-4) var(--openbitfun-space-5);
1196+
}
1197+
1198+
.openbitfun-remote-connect__connections-content {
1199+
padding: var(--openbitfun-space-4) var(--openbitfun-space-5);
1200+
border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
1201+
}
1202+
1203+
.openbitfun-remote-connect__connections-heading {
1204+
display: flex;
1205+
flex-wrap: wrap;
1206+
align-items: center;
1207+
justify-content: space-between;
1208+
gap: var(--openbitfun-space-2);
1209+
1210+
h4 {
1211+
margin: 0;
1212+
color: var(--openbitfun-color-content-secondary);
1213+
font-size: var(--openbitfun-type-label-sm-font-size);
1214+
font-weight: var(--openbitfun-type-label-selected-font-weight);
1215+
line-height: var(--openbitfun-type-body-sm-line-height);
1216+
}
1217+
}
1218+
1219+
.openbitfun-remote-connect__connections-list {
1220+
list-style: none;
1221+
padding: 0;
1222+
margin: var(--openbitfun-space-2) 0 0;
1223+
max-height: 132px;
1224+
overflow-y: auto;
1225+
1226+
li {
1227+
display: flex;
1228+
align-items: center;
1229+
gap: var(--openbitfun-space-3);
1230+
padding-block: var(--openbitfun-space-2);
1231+
color: var(--openbitfun-color-content-secondary);
1232+
font-size: var(--openbitfun-type-body-sm-font-size);
1233+
line-height: var(--openbitfun-type-body-sm-line-height);
1234+
}
1235+
1236+
strong {
1237+
flex: 1;
1238+
min-width: 0;
1239+
overflow-wrap: anywhere;
1240+
color: var(--openbitfun-color-content-primary);
1241+
font-weight: var(--openbitfun-type-label-selected-font-weight);
1242+
}
1243+
}
1244+
1245+
.openbitfun-remote-connect__connections-note {
1246+
margin: var(--openbitfun-space-2) 0 0;
1247+
color: var(--openbitfun-color-content-secondary);
1248+
font-size: var(--openbitfun-type-body-sm-font-size);
1249+
line-height: var(--openbitfun-type-body-sm-line-height);
1250+
}
1251+
1252+
.openbitfun-remote-connect__relay-actions {
1253+
padding: var(--openbitfun-space-3) var(--openbitfun-space-5);
1254+
border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
1255+
}
1256+
1257+
.openbitfun-remote-connect__relay-action-row {
1258+
display: flex;
1259+
flex-wrap: wrap;
1260+
justify-content: space-between;
1261+
align-items: center;
1262+
gap: var(--openbitfun-space-3);
1263+
}
1264+
1265+
.openbitfun-remote-connect__relay-invitation {
1266+
padding: var(--openbitfun-space-4) var(--openbitfun-space-5);
1267+
border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
1268+
1269+
.openbitfun-remote-connect__pairing-card {
1270+
margin: 0;
1271+
padding: 0;
1272+
background: transparent;
1273+
}
1274+
}

0 commit comments

Comments
 (0)