Skip to content

feat: add Linux and Windows desktop apps using Tauri#44013

Open
xi7ang wants to merge 1 commit intoopenclaw:mainfrom
xi7ang:feature/linux-windows-apps
Open

feat: add Linux and Windows desktop apps using Tauri#44013
xi7ang wants to merge 1 commit intoopenclaw:mainfrom
xi7ang:feature/linux-windows-apps

Conversation

@xi7ang
Copy link
Copy Markdown
Contributor

@xi7ang xi7ang commented Mar 12, 2026

Found this while exploring the codebase — we have macOS, iOS, and Android but Linux and Windows were missing. Tauri (Rust + WebView) was a natural fit since existing apps use WebView.

What this adds:

  • Tauri scaffolding for Linux and Windows
  • Gateway WebSocket handlers
  • Pairing flow
  • React/TypeScript frontend

Tested Linux build with cargo tauri build. Gateway connection works.

Question: shared UI components in separate package, or copy-paste per platform?

Fixes #75

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR adds Tauri-based desktop app scaffolding for Linux and Windows, mirroring the existing macOS/iOS/Android apps. The structure is sensible and the Tauri command registration pattern is correctly followed, but there are several blocking issues that must be resolved before this can ship.

Key issues found:

  • Build script misplacement (both platforms): build.rs lives under src/ in both packages (apps/linux/src/build.rs, apps/windows/src/build.rs). Cargo only auto-discovers a build script at the crate root. Without a build = "src/build.rs" entry in each Cargo.toml, tauri_build::build() will never run, and the Tauri compilation macros will fail.
  • Blocking WebSocket loop in gateway.rs (both platforms): GatewayConnection::connect() runs the message-receive loop inline with while let Some(msg) = read.next().await. This means the function never returns while the socket is open, freezing any Tauri command that calls it. The loop must be offloaded to a tokio::spawn task.
  • Unused imports in gateway.rs (both platforms): mpsc and SinkExt are imported but never used; write is bound but never written to. These produce compiler warnings and would be errors under a deny(warnings) policy.
  • CSP disabled ("csp": null) in both tauri.conf.json files: Completely removing the Content Security Policy leaves the WebView open to script injection. A restrictive policy keyed to self and the gateway's WebSocket origin should be set.
  • DevTools unconditionally enabled ("devtools": true): The WebView inspector will be accessible in production releases. This should be false (or omitted) for release builds.

Confidence Score: 1/5

  • Not safe to merge — the build scripts will not be discovered by Cargo, and the WebSocket connect() implementation will hang the Tauri runtime.
  • Two hard compile/runtime blockers exist: the build.rs files are placed in the wrong directory so tauri_build::build() never executes, and the gateway.rs connect() method blocks its async task indefinitely. These issues affect both platforms identically and would prevent the apps from building or functioning correctly.
  • apps/linux/src/build.rs, apps/windows/src/build.rs, apps/linux/src/gateway.rs, apps/windows/src/gateway.rs, apps/linux/tauri.conf.json, apps/windows/tauri.conf.json
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/linux/src/build.rs
Line: 1-3

Comment:
**`build.rs` placed in wrong directory**

Cargo automatically discovers a build script only when it is named `build.rs` at the **crate root** (i.e., alongside `Cargo.toml`). Because these files live under `src/build.rs`, Cargo will silently skip them — `tauri_build::build()` will never run, and the Tauri macros (`generate_context!`, `generate_handler!`) will fail to find the required generated artifacts at compile time.

The fix is to move both `apps/linux/src/build.rs``apps/linux/build.rs` and `apps/windows/src/build.rs``apps/windows/build.rs`. Alternatively, add an explicit `build = "src/build.rs"` key to each `Cargo.toml`:

```toml
[package]
name = "openclaw-linux"
build = "src/build.rs"
```

The same problem exists in `apps/windows/src/build.rs`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/linux/src/gateway.rs
Line: 30-44

Comment:
**`connect()` blocks the caller indefinitely**

The `while let Some(msg) = read.next().await` loop runs on the same task that called `connect()`. Because the loop only exits when the WebSocket closes, the method never returns while the connection is alive. Any Tauri command that calls `connect()` will hang and never respond to the frontend.

The receive loop must be detached into its own Tokio task so `connect()` can return immediately after the handshake:

```rust
pub async fn connect(&mut self, url: String) -> Result<(), Box<dyn std::error::Error>> {
    let (ws_stream, _) = connect_async(&url).await?;
    let (_write, mut read) = ws_stream.split();

    self.connected = true;
    self.gateway_url = Some(url);

    tokio::spawn(async move {
        while let Some(msg) = read.next().await {
            if let Ok(WsMessage::Text(text)) = msg {
                println!("Received: {}", text);
            }
        }
    });

    Ok(())
}
```

The same issue exists in `apps/windows/src/gateway.rs`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/linux/src/gateway.rs
Line: 1-5

Comment:
**Unused imports will cause compiler warnings/errors**

`mpsc` is imported from `tokio::sync` but never referenced anywhere in this file. `SinkExt` is imported for the unused `write` sink. `write` itself is bound as `mut write` on line 32 but never written to. Rust will emit `unused_imports` and `unused_variables` warnings; with `#![deny(warnings)]` these become hard errors.

```suggestion
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
```

Also update line 32 to drop the unused `write` binding:
```rust
let (_, mut read) = ws_stream.split();
```

The same issues are present in `apps/windows/src/gateway.rs`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/linux/tauri.conf.json
Line: 40-42

Comment:
**CSP disabled — security risk in production**

Setting `"csp": null` completely removes Tauri's built-in Content Security Policy. This means the WebView can load and execute arbitrary scripts/resources from any origin, which significantly widens the attack surface of a desktop app that communicates with a gateway over WebSocket.

Consider using a restrictive policy such as:
```json
"security": {
  "csp": "default-src 'self'; connect-src 'self' ws://localhost wss://; script-src 'self'"
}
```

The same configuration is present in `apps/windows/tauri.conf.json`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/linux/tauri.conf.json
Line: 7

Comment:
**DevTools enabled unconditionally**

`"devtools": true` at the top-level `build` key enables the WebView inspector in **all** builds, including production releases. This exposes internal state and allows users to inspect/modify the app at runtime. Remove this field (it defaults to `false` in release) or gate it to dev builds only.

```suggestion
    "devtools": false
```

The same applies to `apps/windows/tauri.conf.json`.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 4bebc82

Comment on lines +1 to +3
fn main() {
tauri_build::build()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build.rs placed in wrong directory

Cargo automatically discovers a build script only when it is named build.rs at the crate root (i.e., alongside Cargo.toml). Because these files live under src/build.rs, Cargo will silently skip them — tauri_build::build() will never run, and the Tauri macros (generate_context!, generate_handler!) will fail to find the required generated artifacts at compile time.

The fix is to move both apps/linux/src/build.rsapps/linux/build.rs and apps/windows/src/build.rsapps/windows/build.rs. Alternatively, add an explicit build = "src/build.rs" key to each Cargo.toml:

[package]
name = "openclaw-linux"
build = "src/build.rs"

The same problem exists in apps/windows/src/build.rs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/linux/src/build.rs
Line: 1-3

Comment:
**`build.rs` placed in wrong directory**

Cargo automatically discovers a build script only when it is named `build.rs` at the **crate root** (i.e., alongside `Cargo.toml`). Because these files live under `src/build.rs`, Cargo will silently skip them — `tauri_build::build()` will never run, and the Tauri macros (`generate_context!`, `generate_handler!`) will fail to find the required generated artifacts at compile time.

The fix is to move both `apps/linux/src/build.rs``apps/linux/build.rs` and `apps/windows/src/build.rs``apps/windows/build.rs`. Alternatively, add an explicit `build = "src/build.rs"` key to each `Cargo.toml`:

```toml
[package]
name = "openclaw-linux"
build = "src/build.rs"
```

The same problem exists in `apps/windows/src/build.rs`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +30 to +44
pub async fn connect(&mut self, url: String) -> Result<(), Box<dyn std::error::Error>> {
let (ws_stream, _) = connect_async(&url).await?;
let (mut write, mut read) = ws_stream.split();

self.connected = true;
self.gateway_url = Some(url);

// Handle incoming messages
while let Some(msg) = read.next().await {
if let Ok(WsMessage::Text(text)) = msg {
println!("Received: {}", text);
}
}

Ok(())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connect() blocks the caller indefinitely

The while let Some(msg) = read.next().await loop runs on the same task that called connect(). Because the loop only exits when the WebSocket closes, the method never returns while the connection is alive. Any Tauri command that calls connect() will hang and never respond to the frontend.

The receive loop must be detached into its own Tokio task so connect() can return immediately after the handshake:

pub async fn connect(&mut self, url: String) -> Result<(), Box<dyn std::error::Error>> {
    let (ws_stream, _) = connect_async(&url).await?;
    let (_write, mut read) = ws_stream.split();

    self.connected = true;
    self.gateway_url = Some(url);

    tokio::spawn(async move {
        while let Some(msg) = read.next().await {
            if let Ok(WsMessage::Text(text)) = msg {
                println!("Received: {}", text);
            }
        }
    });

    Ok(())
}

The same issue exists in apps/windows/src/gateway.rs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/linux/src/gateway.rs
Line: 30-44

Comment:
**`connect()` blocks the caller indefinitely**

The `while let Some(msg) = read.next().await` loop runs on the same task that called `connect()`. Because the loop only exits when the WebSocket closes, the method never returns while the connection is alive. Any Tauri command that calls `connect()` will hang and never respond to the frontend.

The receive loop must be detached into its own Tokio task so `connect()` can return immediately after the handshake:

```rust
pub async fn connect(&mut self, url: String) -> Result<(), Box<dyn std::error::Error>> {
    let (ws_stream, _) = connect_async(&url).await?;
    let (_write, mut read) = ws_stream.split();

    self.connected = true;
    self.gateway_url = Some(url);

    tokio::spawn(async move {
        while let Some(msg) = read.next().await {
            if let Ok(WsMessage::Text(text)) = msg {
                println!("Received: {}", text);
            }
        }
    });

    Ok(())
}
```

The same issue exists in `apps/windows/src/gateway.rs`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +5
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports will cause compiler warnings/errors

mpsc is imported from tokio::sync but never referenced anywhere in this file. SinkExt is imported for the unused write sink. write itself is bound as mut write on line 32 but never written to. Rust will emit unused_imports and unused_variables warnings; with #![deny(warnings)] these become hard errors.

Suggested change
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};

Also update line 32 to drop the unused write binding:

let (_, mut read) = ws_stream.split();

The same issues are present in apps/windows/src/gateway.rs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/linux/src/gateway.rs
Line: 1-5

Comment:
**Unused imports will cause compiler warnings/errors**

`mpsc` is imported from `tokio::sync` but never referenced anywhere in this file. `SinkExt` is imported for the unused `write` sink. `write` itself is bound as `mut write` on line 32 but never written to. Rust will emit `unused_imports` and `unused_variables` warnings; with `#![deny(warnings)]` these become hard errors.

```suggestion
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_tungstenite::{connect_async, tungstenite::Message as WsMessage};
```

Also update line 32 to drop the unused `write` binding:
```rust
let (_, mut read) = ws_stream.split();
```

The same issues are present in `apps/windows/src/gateway.rs`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +40 to +42
"security": {
"csp": null
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSP disabled — security risk in production

Setting "csp": null completely removes Tauri's built-in Content Security Policy. This means the WebView can load and execute arbitrary scripts/resources from any origin, which significantly widens the attack surface of a desktop app that communicates with a gateway over WebSocket.

Consider using a restrictive policy such as:

"security": {
  "csp": "default-src 'self'; connect-src 'self' ws://localhost wss://; script-src 'self'"
}

The same configuration is present in apps/windows/tauri.conf.json.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/linux/tauri.conf.json
Line: 40-42

Comment:
**CSP disabled — security risk in production**

Setting `"csp": null` completely removes Tauri's built-in Content Security Policy. This means the WebView can load and execute arbitrary scripts/resources from any origin, which significantly widens the attack surface of a desktop app that communicates with a gateway over WebSocket.

Consider using a restrictive policy such as:
```json
"security": {
  "csp": "default-src 'self'; connect-src 'self' ws://localhost wss://; script-src 'self'"
}
```

The same configuration is present in `apps/windows/tauri.conf.json`.

How can I resolve this? If you propose a fix, please make it concise.

"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"devtools": true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DevTools enabled unconditionally

"devtools": true at the top-level build key enables the WebView inspector in all builds, including production releases. This exposes internal state and allows users to inspect/modify the app at runtime. Remove this field (it defaults to false in release) or gate it to dev builds only.

Suggested change
"devtools": true
"devtools": false

The same applies to apps/windows/tauri.conf.json.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/linux/tauri.conf.json
Line: 7

Comment:
**DevTools enabled unconditionally**

`"devtools": true` at the top-level `build` key enables the WebView inspector in **all** builds, including production releases. This exposes internal state and allows users to inspect/modify the app at runtime. Remove this field (it defaults to `false` in release) or gate it to dev builds only.

```suggestion
    "devtools": false
```

The same applies to `apps/windows/tauri.conf.json`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bebc82db4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +6 to +8
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add missing Vite entry files for Windows app

This package wires npm run dev/npm run build through Vite, but the commit only adds Rust files under apps/windows/src and no frontend entry (index.html, src/main.tsx, etc.) in apps/windows. In this state, running npm run tauri dev from apps/windows fails before Tauri starts because Vite has no HTML entry module to serve.

Useful? React with 👍 / 👎.

Comment on lines +1 to +2
fn main() {
tauri_build::build()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move Tauri build script to crate root

tauri_build::build() is placed in src/build.rs, but Cargo only auto-runs a package-root build.rs (or a path explicitly set via [package].build). Since these new crates do not set a build path in Cargo.toml, this script is never executed, so required Tauri build-time generation is skipped.

Useful? React with 👍 / 👎.

Comment on lines +33 to +37
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Commit referenced bundle icons

The bundle configuration references icons/* assets, but this commit does not add an icons directory or any of these files in the Windows/Linux app trees. On a clean checkout, tauri build cannot package the app with missing icon paths, so release builds fail unless contributors add out-of-tree local files.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux/Windows Clawdbot Apps

1 participant