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
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "my-project",
"version": "0.1.41",
"version": "0.1.42",
"private": true,
"scripts": {
"dev": "vite --host 0.0.0.0 --port 3000",
Expand Down
4 changes: 2 additions & 2 deletions client/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ resolver = "2"

[package]
name = "xero-desktop"
version = "0.1.41"
version = "0.1.42"
edition = "2021"
default-run = "xero-desktop"
description = "Xero desktop host"
Expand Down
2 changes: 1 addition & 1 deletion client/src-tauri/crates/xero-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "xero-cli"
version = "0.1.41"
version = "0.1.42"
edition = "2021"
description = "Headless Xero CLI backed by xero-agent-core"

Expand Down
59 changes: 59 additions & 0 deletions client/src-tauri/crates/xero-remote-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,11 @@ where
}
}

pub fn refresh_account_devices(&self) -> BridgeResult<Vec<AccountDevice>> {
self.clear_account_devices_cache()?;
self.list_account_devices()
}

fn list_account_devices_once(&self) -> BridgeResult<Vec<AccountDevice>> {
let Some(identity) = self.identity_store.load()? else {
return Ok(Vec::new());
Expand Down Expand Up @@ -2393,6 +2398,60 @@ mod tests {
.is_none());
}

#[test]
fn refresh_account_devices_bypasses_fresh_cache() {
let (relay_url, server) = serve_http_responses(vec![(
200,
json!({
"devices": [{
"id": "web-2",
"account_id": "account-1",
"kind": "web",
"name": "Xero Web",
"user_agent": "browser",
"last_seen": null,
"created_at": "2026-05-31T00:00:00Z",
"revoked_at": null
}]
})
.to_string(),
)]);
let temp = tempfile_path("device-cache-refresh");
let identity_store = FileIdentityStore::new(temp.join("identity.json"));
let identity = test_identity();
identity_store.save(&identity).expect("identity");
let bridge = RemoteBridge::new(
BridgeConfig {
relay_url,
device_name: Some("Xero Test".into()),
},
identity_store,
);
bridge
.store_account_devices_cache(
&identity,
vec![AccountDevice {
id: "web-1".into(),
account_id: "account-1".into(),
kind: "web".into(),
name: Some("Old Web".into()),
user_agent: Some("browser".into()),
last_seen: None,
created_at: "2026-05-30T00:00:00Z".into(),
revoked_at: None,
}],
)
.expect("store stale devices");

let devices = bridge.refresh_account_devices().expect("devices");

assert_eq!(devices.len(), 1);
assert_eq!(devices[0].id, "web-2");
let requests = server.join().expect("fake relay thread");
assert_eq!(requests.len(), 1);
assert!(requests[0].starts_with("GET /api/devices "));
}

#[test]
fn list_account_devices_refreshes_expired_token_before_request() {
let (relay_url, server) = serve_http_responses(vec![
Expand Down
71 changes: 59 additions & 12 deletions client/src-tauri/src/commands/remote_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,14 +684,13 @@ fn handle_inbound_command<R: Runtime + 'static>(
}
let result = route_inbound_command(app, state, Arc::clone(&bridge), command.clone());
if let Err(error) = &result {
let _ = bridge.forward_control_event(
&response_session,
json!({
"schema": "xero.remote_command_result.v1",
"ok": false,
"error": error,
}),
);
let mut payload = json!({
"schema": "xero.remote_command_result.v1",
"ok": false,
"error": error,
});
attach_command_context(&mut payload, &command, Some("rejected"));
let _ = bridge.forward_control_event(&response_session, payload);
}
if let Err(error) = &result {
let _ = bridge.forward_control_event(
Expand Down Expand Up @@ -963,10 +962,12 @@ fn route_inbound_command<R: Runtime + 'static>(
fn ensure_known_web_device(bridge: &AppRemoteBridge, device_id: &str) -> CommandResult<()> {
validate_non_empty(device_id, "deviceId")?;
let devices = bridge.list_account_devices().map_err(map_bridge_error)?;
if devices
.iter()
.any(|device| device.kind == "web" && device.revoked_at.is_none() && device.id == device_id)
{
if account_devices_include_web_device(&devices, device_id) {
return Ok(());
}

let devices = bridge.refresh_account_devices().map_err(map_bridge_error)?;
if account_devices_include_web_device(&devices, device_id) {
return Ok(());
}

Expand All @@ -975,6 +976,12 @@ fn ensure_known_web_device(bridge: &AppRemoteBridge, device_id: &str) -> Command
))
}

fn account_devices_include_web_device(devices: &[AccountDevice], device_id: &str) -> bool {
devices
.iter()
.any(|device| device.kind == "web" && device.revoked_at.is_none() && device.id == device_id)
}

fn route_authorize_session_join<R: Runtime>(
app: &AppHandle<R>,
state: &DesktopState,
Expand Down Expand Up @@ -4795,6 +4802,46 @@ mod tests {
assert!(!command_is_duplicate(&command));
}

#[test]
fn known_web_device_check_ignores_revoked_and_desktop_devices() {
let devices = vec![
AccountDevice {
id: "desktop-1".into(),
account_id: "account-1".into(),
kind: "desktop".into(),
name: Some("Xero Desktop".into()),
user_agent: None,
last_seen: None,
created_at: "2026-05-31T00:00:00Z".into(),
revoked_at: None,
},
AccountDevice {
id: "web-1".into(),
account_id: "account-1".into(),
kind: "web".into(),
name: Some("Old Web".into()),
user_agent: Some("browser".into()),
last_seen: None,
created_at: "2026-05-31T00:00:00Z".into(),
revoked_at: Some("2026-05-31T00:05:00Z".into()),
},
AccountDevice {
id: "web-2".into(),
account_id: "account-1".into(),
kind: "web".into(),
name: Some("Xero Web".into()),
user_agent: Some("browser".into()),
last_seen: None,
created_at: "2026-05-31T00:10:00Z".into(),
revoked_at: None,
},
];

assert!(!account_devices_include_web_device(&devices, "desktop-1"));
assert!(!account_devices_include_web_device(&devices, "web-1"));
assert!(account_devices_include_web_device(&devices, "web-2"));
}

#[test]
fn inbound_pointer_coalescing_keeps_latest_contiguous_move() {
let (sender, receiver) = std::sync::mpsc::sync_channel(8);
Expand Down
2 changes: 1 addition & 1 deletion client/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Xero",
"mainBinaryName": "xero-desktop",
"version": "0.1.41",
"version": "0.1.42",
"identifier": "com.hyperpush.xero",
"build": {
"beforeDevCommand": "pnpm dev",
Expand Down
67 changes: 67 additions & 0 deletions cloud/src/routes/-desktop-click-ripple.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,73 @@ describe("ComputerUseDesktopViewport click feedback", () => {
).toBeTruthy();
});

it("surfaces desktop-side stream start failures after the relay accepts Start", async () => {
let frameHandler: ((rawFrame: unknown) => void) | null = null;
const push = vi.fn();
const channel = {
on: vi.fn((event: string, handler: (rawFrame: unknown) => void) => {
if (event === "frame") frameHandler = handler;
return "frame-ref";
}),
off: vi.fn(),
push,
} as unknown as Channel;

render(
<ComputerUseDesktopViewport
channel={channel}
computerId="desktop-1"
deviceId="web-1"
iceServers={[]}
isAgentWorking={false}
isOnline
onPromptSubmit={vi.fn()}
previewUrl={null}
presentation={{
isMobile: false,
override: "desktop",
rotateDesktop: false,
}}
sessionId="session-1"
streamRunId="run-1"
streamToken="stream-token-1"
/>,
);

const desktop = screen.getByLabelText("Desktop");
const toolbar = within(desktop).getByRole("toolbar", {
name: "Desktop stream controls",
});
fireEvent.click(within(toolbar).getByRole("button", { name: /start/i }));
expect(screen.getByText("Connecting stream")).toBeTruthy();

act(() => {
frameHandler?.(
relayFrame({
schema: "xero.remote_command_result.v1",
ok: false,
kind: "computer_use_stream_request",
outcome: "rejected",
error: {
code: "policy_denied",
message:
"Remote command rejected because the web device is not linked or has been revoked.",
},
}),
);
});

expect(await screen.findByText("Desktop unavailable")).toBeTruthy();
expect(
screen.getByText(
"Remote command rejected because the web device is not linked or has been revoked.",
),
).toBeTruthy();
expect(
within(toolbar).getByRole("button", { name: /start/i }),
).toBeTruthy();
});

it("retries the desktop stream request when connecting stalls before media arrives", () => {
vi.useFakeTimers();
try {
Expand Down
Loading