Hacking Tauri for Designer

wdym?

Now I can open up Tauri frontend inside the browser, like this:

And all functionality works the same as using it inside a normal app window.

You might be thinking:

Why is it something to do with the Designer? Tauri is some kind of Devtool, no?

To help them design & iterate fast right inside the codebase, with coding agents.

Tauri is using Webview, so running it in any type of browser should work out-of-the-box, no?

Actually, that’s not the case. We had to build fastrepl/char/plugins/relay to enable it.

motivation

Like Ryo Lu said, the line between "developer" and "designer" is definitely getting blurry.

He even showed how he works, using something called Baby Cursor, within the "Cursor's integrated browser":

The End of the Designer–Engineer Divide
The End of the Designer–Engineer Divide

After watching this, I wanted to enable something similar to everyone who works in fastrepl/char.

And I first thought it would be simple. Tauri allows typical web frontend stacks to draw UI inside the Webview. So I could just open up localhost:1422 inside the Cursor, right?

Actually, it was not.

problem

solution

fastrepl/char/plugins/relay does it. fastrepl/char#4007 is almost all it takes. (I pushed a bit more commits after this though)

How it works:

flowchart LR
  Browser -->|invoke| Shim[shim.js]
  Shim -->|WebSocket| Relay[Relay]
  Relay --> Backend[Tauri]
  Backend -->|response| Browser
plugins/relay/js/shim.js L151-L172view on github
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
  window.__TAURI_INTERNALS__ = {
    metadata: metadata,
    _metadata: metadata,
    plugins: {
      path: {
        sep: env.isWindows ? "\\" : "/",
        delimiter: env.isWindows ? ";" : ":",
      },
    },
    invoke: relay.invoke,
    transformCallback: function (callback, once) {
      var id = relay.nextId();
      window["_" + id] = function (response) {
        if (once) delete window["_" + id];
        if (callback) callback(response);
      };
      return id;
    },
    convertFileSrc: function (path) {
      return path;
    },
  };

At this point, the problem is solved. But there is one more concern left.

  1. tauri dev command requires Rust and other dependencies.
  2. most people are familiar with running some web app, but setting up Rust and waiting build is not very approachable. It need lots of compute resource, storage, and time.

Then I realized that we have a Staging build that we don't distribute, but just exists for testing. Staging is built with devtools which enables custom devtools we built(like seeding data etc) and right click context menu in webview(like inspect menu).

.github/workflows/desktop_cd.yaml L133-L137view on github
133
134
135
136
137
          FEATURES_FLAG=""
          if [[ "${{ inputs.channel }}" == "staging" ]]; then
            FEATURES_FLAG="--features devtools"
          fi
          pnpm -F desktop tauri build --target ${{ matrix.target }} --config ${{ env.TAURI_CONF_PATH }} --verbose $FEATURES_FLAG

Now anyone with the Staging build can just run it as Rust backend, and make code changes in TypeScript side for UI iteration.

3123b95view on github
apps/desktop/package.json
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
+ "dev:relay": "echo 'Make sure Char Staging app is running, then open http://localhost:1423' && vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "dotenvx run --ignore MISSING_ENV_FILE -f ../../.env.supabase -f .env -- tauri",
apps/desktop/src-tauri/src/lib.rs
@@ -119,7 +119,6 @@ pub async fn main() {
.plugin(tauri_plugin_windows::init())
.plugin(tauri_plugin_js::init())
.plugin(tauri_plugin_flag::init())
- .plugin(tauri_plugin_relay::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_listener::init())
.plugin(tauri_plugin_listener2::init())
@@ -149,6 +148,11 @@ pub async fn main() {
builder = builder.plugin(tauri_plugin_sentry::init_with_no_injection(client));
}
+ #[cfg(any(debug_assertions, feature = "devtools"))]
+ {
+ builder = builder.plugin(tauri_plugin_relay::init());
+ }
+
#[cfg(all(not(debug_assertions), not(feature = "devtools")))]
{
let plugin = tauri_plugin_prevent_default::init();