feat(desktop): native terminal session driver (VAPP-87) #96

Merged
longjacksonle merged 1 commit from feat/VAPP-87-native-transport-terminal into main 2026-07-18 02:14:38 +02:00

What

First native desktop session driver (VAPP-87). The desktop app ships every release but its Terminal/Files/Remote-desktop panes were wasm-only and gated to "unavailable, use the web console" (VAPP-86). This makes the Terminal pane open a live session on the desktop build, matching the web console.

How

  • Shared transport (native_relay::connect): dials vervain-server's relay with tokio-tungstenite (ws:// or wss://, rustls) and splits it into a write/read half. Reusable by the Files and Remote-desktop drivers next.
  • Terminal driver (imp_native in terminal.rs): drives the same vendored xterm.js inside the desktop webview over the document::eval bidirectional bridge, speaking the identical relay wire protocol as the wasm path ('c' rendezvous -> "1" + options envelope; PTY Binary frames -> term.write; onData -> Binary frames; onResize -> termsize control envelope). Runs as a Dioxus spawn task (polled inside dioxus-desktop's tokio runtime, so tokio I/O works); dropping the pane cancels it, drops the socket, and the agent kills the PTY.
  • cfg split: TermHandle is now three-way: wasm (unchanged), desktop-native (new), no-op SSR stub (not(feature = "desktop")).
  • Browser-only controls: Document-Picture-in-Picture detach and the Keyboard Lock capture toggle don't exist in the webview, so the pane hides those toolbar buttons on desktop instead of shipping controls that do nothing.
  • VAPP-86 gate: removed from the Terminal pane and Terminal dropped from tunnel_gate_guard's PANES, so the guard and the pane can't drift. Files and Remote-desktop keep their gate until their native drivers land.

No new dependencies: tokio-tungstenite (rustls, ws+wss) and futures-util were already in the non-wasm dependency set.

Verification

  • cargo check + cargo clippy on the desktop feature: green.
  • Containerized CI gate (just pre-commit): fmt, clippy --features server -D warnings, server check, check --target wasm32-unknown-unknown, and all 102 tests (including the updated tunnel_gate_guard) pass.
  • Remaining manual step: end-to-end GUI smoke test on a running desktop build against the dev stack (open a device, open Terminal, confirm a live shell). Not automatable here (no display).

Scope

PR1 of 3 for VAPP-87. Files and Remote-desktop follow on the same shared transport.

🤖 Generated with Claude Code

## What First native desktop session driver (VAPP-87). The desktop app ships every release but its Terminal/Files/Remote-desktop panes were wasm-only and gated to "unavailable, use the web console" (VAPP-86). This makes the **Terminal** pane open a live session on the desktop build, matching the web console. ## How - **Shared transport** (`native_relay::connect`): dials vervain-server's relay with `tokio-tungstenite` (ws:// or wss://, rustls) and splits it into a write/read half. Reusable by the Files and Remote-desktop drivers next. - **Terminal driver** (`imp_native` in `terminal.rs`): drives the same vendored xterm.js inside the desktop webview over the `document::eval` bidirectional bridge, speaking the identical relay wire protocol as the wasm path (`'c'` rendezvous -> `"1"` + `options` envelope; PTY Binary frames -> `term.write`; `onData` -> Binary frames; `onResize` -> `termsize` control envelope). Runs as a Dioxus `spawn` task (polled inside dioxus-desktop's tokio runtime, so tokio I/O works); dropping the pane cancels it, drops the socket, and the agent kills the PTY. - **cfg split**: `TermHandle` is now three-way: wasm (unchanged), desktop-native (new), no-op SSR stub (`not(feature = "desktop")`). - **Browser-only controls**: Document-Picture-in-Picture detach and the Keyboard Lock capture toggle don't exist in the webview, so the pane hides those toolbar buttons on desktop instead of shipping controls that do nothing. - **VAPP-86 gate**: removed from the Terminal pane and Terminal dropped from `tunnel_gate_guard`'s `PANES`, so the guard and the pane can't drift. Files and Remote-desktop keep their gate until their native drivers land. No new dependencies: `tokio-tungstenite` (rustls, ws+wss) and `futures-util` were already in the non-wasm dependency set. ## Verification - `cargo check` + `cargo clippy` on the `desktop` feature: green. - Containerized CI gate (`just pre-commit`): fmt, `clippy --features server -D warnings`, server check, `check --target wasm32-unknown-unknown`, and all 102 tests (including the updated `tunnel_gate_guard`) pass. - Remaining manual step: end-to-end GUI smoke test on a running desktop build against the dev stack (open a device, open Terminal, confirm a live shell). Not automatable here (no display). ## Scope PR1 of 3 for VAPP-87. Files and Remote-desktop follow on the same shared transport. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(desktop): native terminal session driver over tokio-tungstenite + eval (VAPP-87)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 35s
Create release / Create release from merged PR (pull_request) Has been skipped
5a0976c45d
The desktop app ships every release but its terminal/files/remote-desktop panes were wasm-only and gated to say "unavailable, use the web console" (VAPP-86). This lands the first native driver: the Terminal pane now opens a live session on the desktop build, matching the web console.

Shared transport: `native_relay::connect` dials vervain-server's relay with `tokio-tungstenite` (ws:// or wss://, rustls) and splits it into a write/read half. This is the reusable half the files and remote-desktop drivers will sit on next.

Terminal driver: a native `imp_native` module drives the same vendored xterm.js inside the desktop webview over the `document::eval` bidirectional bridge, and speaks the identical relay wire protocol as the wasm path: the relay's `'c'` rendezvous is answered with the Terminal protocol byte `"1"` plus an `options` size envelope, PTY output (Binary frames) becomes `term.write`, keystrokes (`onData`) go out as Binary frames, and `onResize` sends a `termsize` control envelope. The driver runs as a Dioxus `spawn` task (polled inside dioxus-desktop's tokio runtime, so tokio I/O works); dropping the pane cancels it, which drops the socket and lets the agent kill the PTY.

The `TermHandle` is now a three-way split by cfg: wasm (unchanged), desktop-native (this), and a no-op SSR stub (`not(feature = "desktop")`). Document-Picture-in-Picture detach and the Keyboard Lock capture toggle are browser-only, so the pane hides those toolbar buttons on desktop rather than shipping controls that do nothing; the handle keeps no-op methods behind them only so the shared rsx compiles.

VAPP-86 gate: removed from the Terminal pane, and Terminal dropped from `tunnel_gate_guard`'s `PANES` so the guard and the pane cannot drift. Files and remote desktop keep their gate until their native drivers land.

Verified: cargo check + clippy on the desktop feature, the containerized CI gate (fmt, clippy --features server -D warnings, server check, wasm check, tests) all green. End-to-end GUI smoke test on a running desktop build is the remaining manual step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BUfsRDQa56XWH2QKPaeRim
longjacksonle deleted branch feat/VAPP-87-native-transport-terminal 2026-07-18 02:14:38 +02:00
Author
Owner

Closing: wrong premise. Live testing on a real desktop run showed the "desktop" app runs the wasm web client inside a webview (http://127.0.0.1:<port> serving vervain-app_bg.wasm), not native dioxus-desktop rendering. So web_sys is present, Terminal (xterm.js) loads, and the wasm session drivers execute - the relay even connects and rendezvouses with the agent (server logs: stashed -> joined existing relay -> ate protocol byte).

That invalidates the VAPP-87 premise this PR is built on ("dioxus-desktop runs natively, so web_sys is absent, rewrite the drivers in Rust"). A native document::eval + tokio-tungstenite driver never executes in this architecture - it's dead code.

The genuine bug is wasm-side: the Terminal pane churns/reconnects in the webview (new tunnel every few seconds, WebSocket CLOSING/CLOSED, blank pane). Re-investigating that under VAPP-87 with the corrected understanding. Not merging this.

Closing: wrong premise. Live testing on a real desktop run showed the "desktop" app runs the **wasm web client inside a webview** (`http://127.0.0.1:<port>` serving `vervain-app_bg.wasm`), not native dioxus-desktop rendering. So `web_sys` is present, `Terminal` (xterm.js) loads, and the wasm session drivers execute - the relay even connects and rendezvouses with the agent (server logs: `stashed -> joined existing relay -> ate protocol byte`). That invalidates the VAPP-87 premise this PR is built on ("dioxus-desktop runs natively, so web_sys is absent, rewrite the drivers in Rust"). A native `document::eval` + tokio-tungstenite driver never executes in this architecture - it's dead code. The genuine bug is wasm-side: the Terminal pane churns/reconnects in the webview (new tunnel every few seconds, `WebSocket CLOSING/CLOSED`, blank pane). Re-investigating that under VAPP-87 with the corrected understanding. Not merging this.
Author
Owner

Closing: wrong premise. Live testing on a real desktop run showed the "desktop" app runs the wasm web client inside a webview (http://127.0.0.1: serving vervain-app_bg.wasm), not native dioxus-desktop rendering. So web_sys is present, xterm.js loads, and the wasm session drivers execute; the relay even connects and rendezvouses with the agent (server logs: stashed -> joined existing relay -> ate protocol byte).

That invalidates the VAPP-87 premise this PR is built on ("dioxus-desktop runs natively, so web_sys is absent, rewrite the drivers in Rust"). A native document::eval + tokio-tungstenite driver never executes in this architecture.

The genuine bug is wasm-side: the Terminal pane churns/reconnects in the webview (new tunnel every few seconds, WebSocket CLOSING/CLOSED, blank pane). Re-investigating under VAPP-87 with the corrected understanding.

Closing: wrong premise. Live testing on a real desktop run showed the "desktop" app runs the wasm web client inside a webview (http://127.0.0.1:<port> serving vervain-app_bg.wasm), not native dioxus-desktop rendering. So web_sys is present, xterm.js loads, and the wasm session drivers execute; the relay even connects and rendezvouses with the agent (server logs: stashed -> joined existing relay -> ate protocol byte). That invalidates the VAPP-87 premise this PR is built on ("dioxus-desktop runs natively, so web_sys is absent, rewrite the drivers in Rust"). A native document::eval + tokio-tungstenite driver never executes in this architecture. The genuine bug is wasm-side: the Terminal pane churns/reconnects in the webview (new tunnel every few seconds, WebSocket CLOSING/CLOSED, blank pane). Re-investigating under VAPP-87 with the corrected understanding.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-apps!96
No description provided.