You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Opening this first, per CONTRIBUTING.md: this is product direction, not a small bugfix. I have a working prototype on a Mac + iPad. I am not sending a PR until you want this shape — happy to shrink it, drop parts, or throw it away.
Problem
I want to use MonoCode on an iPad as a thin client of the Mac: same sessions, files, and agents, without a second copy of the workspace. That should work on the same Wi-Fi and away from home. I do not want the Tailscale iOS app, a VPN / Network Extension, or a public port.
Proposed behavior
Mac: Settings → Companion. Turn on Local network, Tailscale, or both. Show a 6-digit pairing code and a QR per live route.
iPad: first-run pairing screen. Scan the QR, or type the code + host. After that it is the same React UI, talking to the Mac.
LAN stays independent of Tailscale (same Wi-Fi, no login).
Tailscale is embedded in both apps. Sign in with Google on each device (official login.tailscale.com “Connect this device” page). No Tailscale app on the iPad.
The Mac stays the source of truth. The iPad forwards invoke / listen over a token-gated WebSocket.
This is running here: Mac shows Connected — 1 device, iPad is in the live workspace.
Architecture
flowchart TB
subgraph iPad["iPad companion"]
UI["Same React UI"]
RT["RemoteTransport"]
NWS["Rust companion_ws"]
TSN["tsnet userspace node"]
UI --> RT --> NWS
NWS -->|"LAN TCP"| LAN
NWS -->|"100.x Dial"| TSN
end
subgraph Mac["Mac host"]
L["TCP 0.0.0.0:17233"]
E["tailscale-rs embed — DERP-only, Google SSO"]
H["LocalTransport / host process\nsessions, SQLite, agents, PTY"]
L --> H
E -->|"tcp_listen 100.x:17233"| H
end
LAN -->|"ws:// + pairing token"| L
TSN -->|"tailnet"| E
QR / deep link monocode://pair?host=&port=&token=&alt=
or a short-lived 6-digit code, claimed over ?pair=1 (no token yet)
alt carries the other live route so the iPad can switch LAN ↔ Tailscale without pairing again
Google SSO:
Mac: tailscale-rsdevice.is_authorized() login URL
iPad: tsnet status AuthURL
both open https://login.tailscale.com/a/… (Connect this device). After Google, tap Connect. Do not stop on the machines list.
Implementation highlights
Kept rebase-friendly on purpose. Desktop boot path stays LocalTransport. New code is additive; existing commands are called, not rewritten. UI call sites swap invoke / listen to src/lib/transport (one import).
Area
What
Dual routes
Listener is always 0.0.0.0:17233. LAN and Tailscale can both be on. Persist 17233:lan,tailscale.
Mac embed
tailscale crate 0.5, desktop-only, Google login, hostname from the Mac. Optional system tailscale serve is off by default.
iPad embed
Go tsnet c-archive (tailscale.com v1.102.3) + Rust FFI. No Network Extension. Hostname monocode-ipad (iOS gethostname is often localhost).
Native WS
WKWebView is https://tauri.localhost, so a JS WebSocket to ws://LAN is mixed content and dies silently. Dial from Rust; JS gets companion-ws events.
Scan
Prefer LAN, then tailnet. Tailnet dial times out instead of sitting on “Connecting…”. Ignore login.tailscale.com QRs so the camera does not open Safari.
iPad chrome
Hide Mac-only settings (keybindings, providers). Disconnect does not show the Mac pairing QR.
New / ios-or-desktop-gated files (sketch):
src/lib/transport/ — protocol, RemoteTransport, pairing store
Mac dual-route QRs (LAN live while Tailscale joins)
iPad pairing camera + Sign in with Google
Limits I would not hide
tailscale-rs is pre-1.0, unaudited, DERP-only. Treat the embedded Mac path as experimental. Pairing token still gates the socket; there is no extra TLS on that path (WireGuard / DERP covers the tailnet).
No MagicDNS from the crate. Pair with the node’s 100.x address.
System Tailscale on the Mac is optional (tailscale serve --tcp), not required.
iPad tsnet has no VPN interface; it only dials.
Ask
Does this direction make sense for MonoCode? If yes, I can send a small first slice (LAN-only pairing, or Mac settings + protocol, or iPad shell) instead of one large PR. If you are already building something else here, I will stop.
Opening this first, per CONTRIBUTING.md: this is product direction, not a small bugfix. I have a working prototype on a Mac + iPad. I am not sending a PR until you want this shape — happy to shrink it, drop parts, or throw it away.
Problem
I want to use MonoCode on an iPad as a thin client of the Mac: same sessions, files, and agents, without a second copy of the workspace. That should work on the same Wi-Fi and away from home. I do not want the Tailscale iOS app, a VPN / Network Extension, or a public port.
Proposed behavior
login.tailscale.com“Connect this device” page). No Tailscale app on the iPad.invoke/listenover a token-gated WebSocket.This is running here: Mac shows Connected — 1 device, iPad is in the live workspace.
Architecture
flowchart TB subgraph iPad["iPad companion"] UI["Same React UI"] RT["RemoteTransport"] NWS["Rust companion_ws"] TSN["tsnet userspace node"] UI --> RT --> NWS NWS -->|"LAN TCP"| LAN NWS -->|"100.x Dial"| TSN end subgraph Mac["Mac host"] L["TCP 0.0.0.0:17233"] E["tailscale-rs embed — DERP-only, Google SSO"] H["LocalTransport / host process\nsessions, SQLite, agents, PTY"] L --> H E -->|"tcp_listen 100.x:17233"| H end LAN -->|"ws:// + pairing token"| L TSN -->|"tailnet"| EWire protocol is JSON over WebSocket:
ws://<host>:17233/v1/connect?token=…{ id, type: "invoke", command, args }{ id, type: "result", ok, payload|error }or{ type: "event", event, payload }Pairing:
monocode://pair?host=&port=&token=&alt=?pair=1(no token yet)altcarries the other live route so the iPad can switch LAN ↔ Tailscale without pairing againGoogle SSO:
tailscale-rsdevice.is_authorized()login URLAuthURLhttps://login.tailscale.com/a/…(Connect this device). After Google, tap Connect. Do not stop on the machines list.Implementation highlights
Kept rebase-friendly on purpose. Desktop boot path stays
LocalTransport. New code is additive; existing commands are called, not rewritten. UI call sites swapinvoke/listentosrc/lib/transport(one import).0.0.0.0:17233. LAN and Tailscale can both be on. Persist17233:lan,tailscale.tailscalecrate 0.5, desktop-only, Google login, hostname from the Mac. Optional systemtailscale serveis off by default.tailscale.comv1.102.3) + Rust FFI. No Network Extension. Hostnamemonocode-ipad(iOSgethostnameis oftenlocalhost).https://tauri.localhost, so a JSWebSockettows://LANis mixed content and dies silently. Dial from Rust; JS getscompanion-wsevents.login.tailscale.comQRs so the camera does not open Safari.New / ios-or-desktop-gated files (sketch):
src/lib/transport/— protocol, RemoteTransport, pairing storesrc-tauri/src/remote.rs,remote_server.rs,remote_dispatch.rssrc-tauri/src/companion_ws.rs— iPad client socketsrc-tauri/src/tailnet_embed.rs— Mac nodesrc-tauri/src/tsnet_mobile.rs+src-tauri/tsnet/— iPad nodesrc/surfaces/CompanionPage.tsx,CompanionPairing.tsx,QrScanner.tsx,TailnetStatusCard.tsxScreenshots
Captured on a paired Mac + iPad. Source files: alexfoxtm/monocode-companion-shots.
Mac Companion connected (1 device), LAN + Tailscale QRs
iPad workspace after pair
iPad Settings → Companion (client, Connected, monocode-ipad)
Mac Google sign-in
Mac dual-route QRs (LAN live while Tailscale joins)
iPad pairing camera + Sign in with Google
Limits I would not hide
tailscale-rsis pre-1.0, unaudited, DERP-only. Treat the embedded Mac path as experimental. Pairing token still gates the socket; there is no extra TLS on that path (WireGuard / DERP covers the tailnet).100.xaddress.tailscale serve --tcp), not required.Ask
Does this direction make sense for MonoCode? If yes, I can send a small first slice (LAN-only pairing, or Mac settings + protocol, or iPad shell) instead of one large PR. If you are already building something else here, I will stop.