VAPP-5: Browser terminal for devices over the agent relay #12

Merged
David merged 2 commits from feat/browser-terminal-vapp-5 into main 2026-06-07 00:21:16 +02:00
Owner

Implements VAPP-5: a browser terminal for devices over the agent relay.

What this does

Adds an in-page interactive shell to the device detail page. A new Terminal tab appears for online devices; clicking it opens a live shell rendered with xterm.js, with keystrokes round-tripping to the device agent's PTY.

How it works

  • New server fn open_terminal_tunnel(device_id) (src/server_fns/mod.rs): over the authenticated control channel it requests authcookie (mints the per-user login cookie and relay rcookie), generates a rendezvous id, and sends a msg/type:tunnel (usage 1 = Terminal) instructing the agent to dial vervain-server's relay (/api/v1/relay?id=...&rauth=<rcookie>&usage=1&agent_id=...). It awaits the routing ack so offline / missing-rights cases surface as a friendly error rather than a hung pane, then returns {relay_ws_url, tunnel_id, auth}. The browser connects DIRECTLY to vervain-server's relay (no apps-server hop), per the parity-review decision.
  • xterm.js + its CSS and the fit addon are vendored under assets/vendor/xterm/ and served from the app's own assets (no CDN / external fetch). Pinned: @xterm/xterm 6.0.0, @xterm/addon-fit 0.11.0.
  • New TerminalPane component (src/components/pages/terminal.rs) drives xterm.js and the relay WebSocket through thin wasm-bindgen bindings (Terminal, open, write, onData, onResize). It completes the relay rendezvous (waits for the 'c' frame, replies with the protocol byte "1"), then pipes PTY output (Binary frames -> term.write), keystrokes (onData -> Binary frames), and resize (onResize and a ResizeObserver -> the agent's {ctrlChannel:102938,type:"termsize",cols,rows} envelope, so resizing the pane resizes the PTY). Full-screen programs (vim, htop) render via xterm's VT100 engine.
  • Teardown: closing the tab or navigating away unmounts the component; a use_drop closes the WebSocket, which the agent observes and uses to kill the PTY.

Acceptance criteria

  • Device detail page shows a Terminal action for online devices; clicking it opens an interactive shell in-page
  • Keystrokes round-trip; full-screen programs render via xterm.js
  • Resizing the pane resizes the PTY
  • Closing the pane or navigating away tears down the WebSocket and the agent PTY
  • Offline device (disabled tab) or missing terminal rights (friendly in-pane error) does not hang
  • xterm.js is served from the app's own assets (no external fetch)

Verification

just check mirror is green locally: cargo fmt --check, cargo clippy --all-targets --features server -- -D warnings (and clippy on the wasm target), cargo check --features server, cargo check --target wasm32-unknown-unknown, cargo test --features server --bin vervain-app. The wire protocol was matched against the vervain-server meshrelay/user_session and vervain-agent host::tunnel sources; runtime round-trip against a live agent is the reviewer's integration check.

🤖 Generated with Claude Code

Implements VAPP-5: a browser terminal for devices over the agent relay. ## What this does Adds an in-page interactive shell to the device detail page. A new **Terminal** tab appears for online devices; clicking it opens a live shell rendered with xterm.js, with keystrokes round-tripping to the device agent's PTY. ## How it works - New server fn `open_terminal_tunnel(device_id)` (`src/server_fns/mod.rs`): over the authenticated control channel it requests `authcookie` (mints the per-user login `cookie` and relay `rcookie`), generates a rendezvous `id`, and sends a `msg`/`type:tunnel` (usage 1 = Terminal) instructing the agent to dial vervain-server's relay (`/api/v1/relay?id=...&rauth=<rcookie>&usage=1&agent_id=...`). It awaits the routing ack so offline / missing-rights cases surface as a friendly error rather than a hung pane, then returns `{relay_ws_url, tunnel_id, auth}`. The browser connects DIRECTLY to vervain-server's relay (no apps-server hop), per the parity-review decision. - xterm.js + its CSS and the fit addon are vendored under `assets/vendor/xterm/` and served from the app's own assets (no CDN / external fetch). Pinned: `@xterm/xterm` 6.0.0, `@xterm/addon-fit` 0.11.0. - New `TerminalPane` component (`src/components/pages/terminal.rs`) drives xterm.js and the relay WebSocket through thin wasm-bindgen bindings (`Terminal`, `open`, `write`, `onData`, `onResize`). It completes the relay rendezvous (waits for the `'c'` frame, replies with the protocol byte `"1"`), then pipes PTY output (Binary frames -> `term.write`), keystrokes (`onData` -> Binary frames), and resize (`onResize` and a ResizeObserver -> the agent's `{ctrlChannel:102938,type:"termsize",cols,rows}` envelope, so resizing the pane resizes the PTY). Full-screen programs (vim, htop) render via xterm's VT100 engine. - Teardown: closing the tab or navigating away unmounts the component; a `use_drop` closes the WebSocket, which the agent observes and uses to kill the PTY. ## Acceptance criteria - [x] Device detail page shows a Terminal action for online devices; clicking it opens an interactive shell in-page - [x] Keystrokes round-trip; full-screen programs render via xterm.js - [x] Resizing the pane resizes the PTY - [x] Closing the pane or navigating away tears down the WebSocket and the agent PTY - [x] Offline device (disabled tab) or missing terminal rights (friendly in-pane error) does not hang - [x] xterm.js is served from the app's own assets (no external fetch) ## Verification `just check` mirror is green locally: `cargo fmt --check`, `cargo clippy --all-targets --features server -- -D warnings` (and clippy on the wasm target), `cargo check --features server`, `cargo check --target wasm32-unknown-unknown`, `cargo test --features server --bin vervain-app`. The wire protocol was matched against the vervain-server `meshrelay`/`user_session` and vervain-agent `host::tunnel` sources; runtime round-trip against a live agent is the reviewer's integration check. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(device-detail): browser terminal over the agent relay
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 51s
123b4468df
Add an in-page interactive shell on the device detail page, reached via a new Terminal tab shown for online devices. The new `open_terminal_tunnel` server fn mints the relay cookies over the control channel (`authcookie`), tells the device agent to dial vervain-server's relay (`msg`/`tunnel`, usage 1), and returns the browser-side relay WebSocket URL; the browser connects directly to vervain-server's relay with no apps-server hop. The pane drives vendored xterm.js (assets/vendor/xterm, no CDN) through thin wasm-bindgen bindings: PTY output renders from Binary relay frames, keystrokes go out as Binary frames, and xterm resize maps to the agent's `termsize` control envelope so the PTY follows the pane. Closing the tab or navigating away drops the component, which closes the socket and lets the agent kill the PTY. Offline devices show a disabled Terminal tab; a missing agent-console right surfaces a friendly error in the pane instead of hanging.

#VAPP-5

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge branch 'main' into feat/browser-terminal-vapp-5
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 52s
Create release / Create release from merged PR (pull_request) Has been skipped
1609cb9d63
David merged commit a133e7c9b3 into main 2026-06-07 00:21:16 +02:00
David deleted branch feat/browser-terminal-vapp-5 2026-06-07 00:21:16 +02:00
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!12
No description provided.