Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 189 additions & 35 deletions src/apps/desktop/src/api/remote_connect_api.rs

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions src/crates/services/services-integrations/src/remote_connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,13 @@ pub enum RemotePermissionMode {
FullAccess,
}

/// Display-only identity for a live browser connection, never an authorization identity.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RemoteControlClient {
pub id: String,
pub name: String,
}

/// Commands that remote clients can send to the desktop runtime.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
Expand Down Expand Up @@ -2428,7 +2435,10 @@ pub enum RemoteCommand {
device_name: String,
request_id: String,
},
Ping,
Ping {
#[serde(default, skip_serializing_if = "Option::is_none")]
client: Option<RemoteControlClient>,
},

// ── Device-to-device distributed control ──────────────────────────────
//
Expand Down Expand Up @@ -2728,7 +2738,7 @@ where
H: RemoteCommandRuntimeHost + ?Sized,
{
match command {
RemoteCommand::Ping => RemoteResponse::Pong,
RemoteCommand::Ping { .. } => RemoteResponse::Pong,

RemoteCommand::GetWorkspaceInfo
| RemoteCommand::ListRecentWorkspaces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ async fn remote_connect_command_owner_preserves_cancel_and_group_routing() {
assert_eq!(
handle_remote_command(
&host,
&RemoteCommand::Ping,
&RemoteCommand::Ping { client: None },
RemoteConnectSubmissionSource::Relay
)
.await,
Expand Down Expand Up @@ -1786,7 +1786,8 @@ async fn remote_connect_file_command_handler_owns_owner_flow_and_uses_host_root(
&[Some("session-1".to_string())]
);

let error = handle_remote_workspace_file_command(&host, &RemoteCommand::Ping).await;
let error =
handle_remote_workspace_file_command(&host, &RemoteCommand::Ping { client: None }).await;
assert_eq!(
error,
RemoteResponse::Error {
Expand Down Expand Up @@ -3151,3 +3152,26 @@ fn remote_connect_tool_preview_slimming_keeps_short_fields_and_drops_large_strin

assert!(make_slim_tool_params(&serde_json::json!(42)).is_none());
}

#[test]
fn control_ping_accepts_legacy_and_additive_client_identity() {
use openbitfun_services_integrations::remote_connect::RemoteCommand;
let legacy = serde_json::json!({ "cmd": "ping" });
let old: RemoteCommand = serde_json::from_value(legacy.clone()).unwrap();
assert_eq!(old, RemoteCommand::Ping { client: None });
assert_eq!(serde_json::to_value(old).unwrap(), legacy);
let current =
serde_json::json!({ "cmd": "ping", "client": { "id": "page-1", "name": "Safari · iOS" } });
let decoded: RemoteCommand = serde_json::from_value(current.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), current);
// Previous hosts use an internally tagged unit variant and ignore additive fields.
#[derive(serde::Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
enum LegacyCommand {
Ping,
}
assert!(matches!(
serde_json::from_value::<LegacyCommand>(current).unwrap(),
LegacyCommand::Ping
));
}
3 changes: 2 additions & 1 deletion src/mobile-web/src/services/RemoteSessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
RelayHttpClient,
type ControlTargetSnapshot,
} from './RelayHttpClient';
import { getControlClientIdentity } from './controlClientIdentity';

export class RemoteControlTargetChangedError extends Error {
constructor() {
Expand Down Expand Up @@ -571,7 +572,7 @@ export class RemoteSessionManager {
}

async ping(): Promise<void> {
await this.request({ cmd: 'ping' });
await this.request({ cmd: 'ping', client: getControlClientIdentity() });
}

/**
Expand Down
20 changes: 20 additions & 0 deletions src/mobile-web/src/services/controlClientIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
let identity: { id: string; name: string } | undefined;

/** One identity per browser page, shared by managers across target/reconnect changes. */
export function getControlClientIdentity(): { id: string; name: string } {
if (identity) return identity;
const bytes = crypto.getRandomValues(new Uint8Array(16));
const id = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
const ua = navigator.userAgent;
const browser = /Edg\//.test(ua) ? 'Edge'
: /Firefox\/|FxiOS\//.test(ua) ? 'Firefox'
: /Chrome\/|CriOS\//.test(ua) ? 'Chrome'
: /Safari\//.test(ua) ? 'Safari' : 'Browser';
const platform = /iPhone|iPad|iPod/.test(ua) ? 'iOS'
: /Android/.test(ua) ? 'Android'
: /Windows/.test(ua) ? 'Windows'
: /Macintosh/.test(ua) ? 'macOS'
: /Linux/.test(ua) ? 'Linux' : '';
identity = { id, name: [browser, platform].filter(Boolean).join(' · ') };
return identity;
}
7 changes: 2 additions & 5 deletions src/web-ui/src/app/components/NavPanel/NavPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1472,8 +1472,8 @@ $_section-header-height: 22px;
line-height: var(--openbitfun-type-modifier-leading-dense-line-height);
}

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

&[data-openbitfun-device-kind='message-app'] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1190,3 +1190,85 @@
transition: none;
}
}

.openbitfun-remote-connect__relay-address {
padding: var(--openbitfun-space-4) var(--openbitfun-space-5);
}

.openbitfun-remote-connect__connections-content {
padding: var(--openbitfun-space-4) var(--openbitfun-space-5);
border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
}

.openbitfun-remote-connect__connections-heading {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--openbitfun-space-2);

h4 {
margin: 0;
color: var(--openbitfun-color-content-secondary);
font-size: var(--openbitfun-type-label-sm-font-size);
font-weight: var(--openbitfun-type-label-selected-font-weight);
line-height: var(--openbitfun-type-body-sm-line-height);
}
}

.openbitfun-remote-connect__connections-list {
list-style: none;
padding: 0;
margin: var(--openbitfun-space-2) 0 0;
max-height: 132px;
overflow-y: auto;

li {
display: flex;
align-items: center;
gap: var(--openbitfun-space-3);
padding-block: var(--openbitfun-space-2);
color: var(--openbitfun-color-content-secondary);
font-size: var(--openbitfun-type-body-sm-font-size);
line-height: var(--openbitfun-type-body-sm-line-height);
}

strong {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
color: var(--openbitfun-color-content-primary);
font-weight: var(--openbitfun-type-label-selected-font-weight);
}
}

.openbitfun-remote-connect__connections-note {
margin: var(--openbitfun-space-2) 0 0;
color: var(--openbitfun-color-content-secondary);
font-size: var(--openbitfun-type-body-sm-font-size);
line-height: var(--openbitfun-type-body-sm-line-height);
}

.openbitfun-remote-connect__relay-actions {
padding: var(--openbitfun-space-3) var(--openbitfun-space-5);
border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
}

.openbitfun-remote-connect__relay-action-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: var(--openbitfun-space-3);
}

.openbitfun-remote-connect__relay-invitation {
padding: var(--openbitfun-space-4) var(--openbitfun-space-5);
border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);

.openbitfun-remote-connect__pairing-card {
margin: 0;
padding: 0;
background: transparent;
}
}
Loading
Loading