7-phase plan for Go/Wails v3 → Rust/Tauri v2 rewrite. Incorporates Gemini's Rust architecture review: DashMap for session registry, Drop trait for automatic connection cleanup, Tauri asset protocol for RDP frame delivery. Keeps separate SSH exec channel for CWD following (no terminal stream parsing). Includes Go codebase cleanup phase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
789 lines
31 KiB
Markdown
789 lines
31 KiB
Markdown
# Wraith Desktop — Tauri v2 Rewrite Plan
|
|
|
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Rewrite Wraith Desktop from Go/Wails v3 to Rust/Tauri v2, keeping the Vue 3 frontend, to eliminate WebView terminal rendering issues and deliver a production-quality MobaXTerm replacement.
|
|
|
|
**Architecture:** Rust backend (Tauri v2) handles SSH, SFTP, RDP, vault encryption, and host key verification. Vue 3 frontend (Vite + Pinia + Tailwind CSS) runs in WebView2 for all UI chrome. xterm.js renders terminals with proper font loading. SSH data flows via Tauri events (same pattern as current Wails events but with Tauri's more mature IPC). CWD following uses a separate SSH exec channel that polls `pwd` — never touches the terminal data stream. RDP frames served via Tauri asset protocol (`asset://localhost/frame/{id}`) to avoid base64 IPC overhead.
|
|
|
|
**Tech Stack:**
|
|
- **Runtime:** Tauri v2 (stable, not alpha like Wails v3)
|
|
- **Backend:** Rust with `russh` (SSH/SFTP), `rusqlite` (SQLite), `aes-gcm` + `argon2` (vault), `ironrdp` or FreeRDP FFI (RDP), `dashmap` (concurrent session registry)
|
|
- **Frontend:** Vue 3 (Composition API), TypeScript, Vite, Pinia, Tailwind CSS, xterm.js 6
|
|
- **Distribution:** Tauri bundler (NSIS installer for Windows, .dmg for macOS), built-in auto-updater
|
|
|
|
**Key Rust Design Decisions (from Gemini review):**
|
|
- **`DashMap` for session registry** — lock-free concurrent hashmap instead of `RwLock<HashMap>`. No deadlock risk during multi-window tab detach/reattach transitions.
|
|
- **`Drop` trait for session cleanup** — when a session struct is dropped (window closes, disconnect, panic), SSH/SFTP/RDP connections close automatically via Rust's ownership model. No zombie sessions. No "forgot to call disconnect." This is something Go fundamentally cannot do as cleanly.
|
|
- **Tauri asset protocol for RDP frames** — instead of base64-encoding every frame over IPC (current Go approach), register a custom asset protocol handler that serves raw RGBA frames at `asset://localhost/frame/{session_id}`. The `<canvas>` fetches frames as images. Eliminates serialization overhead for 1080p bitmap data.
|
|
- **No OSC 7 terminal stream parsing** — CWD following uses a separate SSH exec channel that polls `pwd`. The terminal data stream is NEVER processed, scanned, or modified. This avoids the ANSI escape sequence corruption that broke the Go version.
|
|
|
|
---
|
|
|
|
## Why Tauri v2 Over Wails v3
|
|
|
|
| Factor | Wails v3 (current) | Tauri v2 (target) |
|
|
|---|---|---|
|
|
| Stability | Alpha — breaking API changes between releases | Stable 2.x — production-ready |
|
|
| Multi-window | Experimental, barely documented | First-class API, well-documented |
|
|
| Auto-updater | None — we built our own (broken) | Built-in with signing, delta updates |
|
|
| IPC | Events + bindings, JSON serialization | Commands + events, serde serialization |
|
|
| Community | Small, Go-centric | Large, active, extensive plugin ecosystem |
|
|
| Binary size | ~15MB (Go runtime) | ~3-5MB (Rust, no runtime) |
|
|
| Startup time | ~500ms (Go GC warmup) | ~100ms |
|
|
|
|
---
|
|
|
|
## Scoped Feature Set (What Gets Built)
|
|
|
|
### In Scope (Launch-Blocking)
|
|
|
|
| Feature | Notes |
|
|
|---|---|
|
|
| SSH terminal (multi-tab) | xterm.js + russh, proper font loading, rAF batching |
|
|
| SFTP sidebar | File tree, upload, download, delete, mkdir, rename |
|
|
| SFTP CWD following | Separate SSH exec channel polls `pwd`, no terminal stream touching |
|
|
| RDP in tabs | ironrdp or FreeRDP FFI, canvas rendering |
|
|
| Encrypted vault | Argon2id + AES-256-GCM, master password, same v1: format |
|
|
| Connection manager | Groups, tree, CRUD, search, tags |
|
|
| Credential management | Passwords + SSH keys, encrypted storage |
|
|
| Host key verification | TOFU model — accept new, block changed |
|
|
| Terminal theming | 7+ built-in themes, per-connection override |
|
|
| Tab detach/reattach | Tauri multi-window — session lives in Rust, view is the window |
|
|
| Syntax highlighting editor | CodeMirror 6 in separate Tauri window |
|
|
| Command palette (Ctrl+K) | Fuzzy search connections + actions |
|
|
| Quick connect | user@host:port in toolbar |
|
|
| Tab badges | Protocol icon, ROOT warning, environment tags |
|
|
| Keyboard shortcuts | Full set per original spec |
|
|
| Auto-updater | Tauri built-in updater with code signing |
|
|
| Status bar | Live terminal dims, connection info, theme name |
|
|
|
|
### Not In Scope
|
|
|
|
| Feature | Reason |
|
|
|---|---|
|
|
| MobaXTerm import | 6 connections — set up by hand |
|
|
| Claude Code plugin | Separate project, post-launch |
|
|
| Plugin system | Not needed without community plugins |
|
|
| Split panes | Post-launch |
|
|
| Session recording | Post-launch |
|
|
| Jump host / bastion | Post-launch |
|
|
|
|
---
|
|
|
|
## Repository Strategy
|
|
|
|
**New repo:** `wraith-v2` (or rename current to `wraith-legacy`, new one becomes `wraith`)
|
|
|
|
The current Go codebase stays intact as reference. No interleaving old and new code. Clean start, clean history.
|
|
|
|
**Go code cleanup** happens in Phase 7 after the Tauri version ships — archive the Go repo, update CI to build from the new repo, remove old Gitea packages.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
```
|
|
wraith-v2/
|
|
src-tauri/ # Rust backend (Tauri v2)
|
|
src/
|
|
main.rs # Tauri app setup, command registration
|
|
lib.rs # Module declarations
|
|
ssh/
|
|
mod.rs # SSH service — connect, disconnect, I/O
|
|
session.rs # SSHSession struct, PTY management
|
|
host_key.rs # Host key store — TOFU verification
|
|
cwd.rs # CWD tracker via separate exec channel
|
|
sftp/
|
|
mod.rs # SFTP operations — list, read, write, delete, mkdir
|
|
rdp/
|
|
mod.rs # RDP service — connect, frame delivery, input
|
|
input.rs # Scancode mapping (JS key → RDP scancode)
|
|
vault/
|
|
mod.rs # Encrypt/decrypt, master password, Argon2id key derivation
|
|
db/
|
|
mod.rs # SQLite connection, migrations
|
|
migrations/ # SQL migration files
|
|
connections/
|
|
mod.rs # Connection CRUD, group management, search
|
|
credentials/
|
|
mod.rs # Credential CRUD, encrypted storage
|
|
settings/
|
|
mod.rs # Key-value settings CRUD
|
|
theme/
|
|
mod.rs # Theme CRUD, built-in theme definitions
|
|
session/
|
|
mod.rs # Session manager — tracks active sessions, tab state
|
|
workspace/
|
|
mod.rs # Workspace snapshot save/load, crash recovery
|
|
commands/
|
|
mod.rs # All #[tauri::command] functions — thin wrappers over services
|
|
ssh_commands.rs # SSH-related commands
|
|
sftp_commands.rs # SFTP-related commands
|
|
rdp_commands.rs # RDP-related commands
|
|
vault_commands.rs # Vault/credential commands
|
|
connection_commands.rs # Connection CRUD commands
|
|
settings_commands.rs # Settings commands
|
|
session_commands.rs # Session manager commands
|
|
Cargo.toml
|
|
tauri.conf.json # Tauri config — windows, permissions, updater
|
|
capabilities/ # Tauri v2 permission capabilities
|
|
icons/ # App icons
|
|
|
|
src/ # Vue 3 frontend (migrated from current)
|
|
App.vue
|
|
main.ts
|
|
layouts/
|
|
MainLayout.vue
|
|
UnlockLayout.vue
|
|
components/ # Largely portable from current codebase
|
|
sidebar/
|
|
ConnectionTree.vue
|
|
SidebarToggle.vue
|
|
session/
|
|
SessionContainer.vue
|
|
TabBar.vue
|
|
TabBadge.vue
|
|
terminal/
|
|
TerminalView.vue
|
|
rdp/
|
|
RdpView.vue
|
|
RdpToolbar.vue
|
|
sftp/
|
|
FileTree.vue
|
|
TransferProgress.vue
|
|
editor/
|
|
EditorWindow.vue
|
|
vault/
|
|
VaultManager.vue
|
|
CredentialForm.vue
|
|
common/
|
|
CommandPalette.vue
|
|
QuickConnect.vue
|
|
StatusBar.vue
|
|
HostKeyDialog.vue
|
|
ThemePicker.vue
|
|
ImportDialog.vue # Kept for future use, not in MVP
|
|
SettingsModal.vue
|
|
ContextMenu.vue
|
|
composables/
|
|
useTerminal.ts # Rewritten — Tauri events instead of Wails
|
|
useSftp.ts # Rewritten — Tauri invoke instead of Wails Call
|
|
useRdp.ts # Rewritten — Tauri invoke/events
|
|
useTransfers.ts # Portable as-is
|
|
stores/
|
|
app.store.ts # Rewritten — Tauri invoke
|
|
connection.store.ts # Rewritten — Tauri invoke
|
|
session.store.ts # Rewritten — Tauri invoke
|
|
assets/
|
|
main.css # Tailwind + custom CSS
|
|
css/
|
|
terminal.css # xterm.js styling
|
|
router/
|
|
index.ts # If needed, otherwise App.vue handles layout switching
|
|
types/
|
|
index.ts # TypeScript interfaces matching Rust structs
|
|
|
|
package.json
|
|
vite.config.ts
|
|
tsconfig.json
|
|
tailwind.config.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation (Tauri Scaffold + SQLite + Vault)
|
|
|
|
### Task 1.1: Tauri v2 Project Scaffold
|
|
|
|
**Files:**
|
|
- Create: `wraith-v2/` entire project via `npm create tauri-app`
|
|
- Create: `src-tauri/tauri.conf.json`
|
|
- Create: `src-tauri/Cargo.toml`
|
|
|
|
- [ ] **Step 1:** Create new project with Tauri v2 CLI
|
|
```bash
|
|
npm create tauri-app@latest wraith-v2 -- --template vue-ts
|
|
cd wraith-v2
|
|
```
|
|
|
|
- [ ] **Step 2:** Configure `tauri.conf.json` — app name "Wraith", window title, default size 1200x800, dark theme, custom icon
|
|
|
|
- [ ] **Step 3:** Add Rust dependencies to `Cargo.toml`:
|
|
```toml
|
|
[dependencies]
|
|
tauri = { version = "2", features = ["protocol-asset"] }
|
|
tauri-plugin-shell = "2"
|
|
tauri-plugin-updater = "2"
|
|
russh = "0.46"
|
|
russh-keys = "0.46"
|
|
russh-sftp = "2"
|
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
|
aes-gcm = "0.10"
|
|
argon2 = "0.5"
|
|
rand = "0.8"
|
|
hex = "0.4"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
uuid = { version = "1", features = ["v4"] }
|
|
base64 = "0.22"
|
|
dashmap = "6"
|
|
tokio = { version = "1", features = ["full"] }
|
|
log = "0.4"
|
|
env_logger = "0.11"
|
|
```
|
|
|
|
- [ ] **Step 4:** Verify `cargo tauri dev` launches an empty window
|
|
|
|
- [ ] **Step 5:** Port the Vue 3 frontend shell — copy `App.vue`, `main.ts`, Tailwind config, CSS variables from current wraith repo. Install frontend deps:
|
|
```bash
|
|
npm install pinia vue-router @xterm/xterm @xterm/addon-fit @xterm/addon-search @xterm/addon-web-links
|
|
npm install -D tailwindcss @tailwindcss/vite
|
|
```
|
|
|
|
- [ ] **Step 6:** Verify `cargo tauri dev` shows the Vue 3 shell with Tailwind styling
|
|
|
|
- [ ] **Step 7:** Commit: `feat: Tauri v2 scaffold with Vue 3 + Tailwind`
|
|
|
|
---
|
|
|
|
### Task 1.2: SQLite Database Layer
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/db/mod.rs`
|
|
- Create: `src-tauri/src/db/migrations/001_initial.sql`
|
|
|
|
- [ ] **Step 1:** Write `001_initial.sql` — same schema as current Go version:
|
|
- `groups` (hierarchical folders)
|
|
- `connections` (hostname, port, protocol, credential_id, tags JSON, options JSON)
|
|
- `credentials` (name, username, type, encrypted_value)
|
|
- `ssh_keys` (name, encrypted_private_key, fingerprint, public_key)
|
|
- `host_keys` (hostname, port, key_type, fingerprint, raw_key — PK on hostname+port+key_type)
|
|
- `settings` (key-value)
|
|
- `themes` (all 20+ color fields)
|
|
- `connection_history` (connection_id, connected_at, disconnected_at, duration_secs)
|
|
|
|
- [ ] **Step 2:** Write `db/mod.rs`:
|
|
- `pub fn open(path: &Path) -> Result<Connection>` — opens SQLite with WAL mode, busy timeout 5000ms, foreign keys ON
|
|
- `pub fn migrate(conn: &Connection) -> Result<()>` — runs embedded SQL migrations
|
|
- Use `rusqlite::Connection` wrapped in `Arc<Mutex<Connection>>` for thread-safe access
|
|
|
|
- [ ] **Step 3:** Write tests — open temp DB, run migrations, verify tables exist
|
|
|
|
- [ ] **Step 4:** Commit: `feat: SQLite database layer with migrations`
|
|
|
|
---
|
|
|
|
### Task 1.3: Vault Service (Encryption)
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/vault/mod.rs`
|
|
|
|
- [ ] **Step 1:** Implement vault service matching current Go encryption exactly:
|
|
- `derive_key(password: &str, salt: &[u8]) -> [u8; 32]` — Argon2id with t=3, m=65536, p=4
|
|
- `generate_salt() -> [u8; 32]` — 32 random bytes via `rand`
|
|
- `encrypt(key: &[u8; 32], plaintext: &str) -> String` — AES-256-GCM, random 12-byte IV, returns `v1:{iv_hex}:{sealed_hex}`
|
|
- `decrypt(key: &[u8; 32], blob: &str) -> Result<String>` — parses v1 format, decrypts
|
|
- `VaultService` struct holds the derived key in memory
|
|
|
|
- [ ] **Step 2:** Write tests — encrypt/decrypt round-trip, wrong key fails, format compatibility with Go-generated blobs
|
|
|
|
- [ ] **Step 3:** **Critical test:** Encrypt a value with the Go version, decrypt it with the Rust version. This ensures the existing wraith.db can be opened by the new app. If formats differ, add a compatibility layer.
|
|
|
|
- [ ] **Step 4:** Commit: `feat: vault encryption — Argon2id + AES-256-GCM`
|
|
|
|
---
|
|
|
|
### Task 1.4: Settings + Connections + Credentials Services
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/settings/mod.rs`
|
|
- Create: `src-tauri/src/connections/mod.rs`
|
|
- Create: `src-tauri/src/credentials/mod.rs`
|
|
|
|
- [ ] **Step 1:** Settings service — `get(key)`, `set(key, value)`, `delete(key)`. UPSERT pattern.
|
|
|
|
- [ ] **Step 2:** Connections service — `create`, `get`, `list`, `update`, `delete`, `create_group`, `list_groups`, `delete_group`, `rename_group`, `search(query)`. Tags via `json_each()`.
|
|
|
|
- [ ] **Step 3:** Credentials service — `create_password`, `create_ssh_key`, `list`, `decrypt_password`, `decrypt_ssh_key`, `delete`. Requires VaultService reference.
|
|
|
|
- [ ] **Step 4:** Write tests for each service — CRUD operations, search, encrypted credential round-trip
|
|
|
|
- [ ] **Step 5:** Commit: `feat: settings, connections, credentials services`
|
|
|
|
---
|
|
|
|
### Task 1.5: Tauri Commands + Frontend Wiring
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/commands/mod.rs`
|
|
- Create: `src-tauri/src/commands/vault_commands.rs`
|
|
- Create: `src-tauri/src/commands/connection_commands.rs`
|
|
- Create: `src-tauri/src/commands/settings_commands.rs`
|
|
- Create: `src-tauri/src/main.rs` (full setup)
|
|
- Migrate: `src/layouts/UnlockLayout.vue` — Wails Call → Tauri invoke
|
|
- Migrate: `src/stores/app.store.ts` — Wails Call → Tauri invoke
|
|
- Migrate: `src/stores/connection.store.ts` — Wails Call → Tauri invoke
|
|
|
|
- [ ] **Step 1:** Write Tauri command wrappers:
|
|
```rust
|
|
#[tauri::command]
|
|
async fn is_first_run(state: State<'_, AppState>) -> Result<bool, String> {
|
|
// ...
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn create_vault(password: String, state: State<'_, AppState>) -> Result<(), String> {
|
|
// ...
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn unlock(password: String, state: State<'_, AppState>) -> Result<(), String> {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2:** Register all commands in `main.rs`:
|
|
```rust
|
|
tauri::Builder::default()
|
|
.manage(AppState::new()?)
|
|
.invoke_handler(tauri::generate_handler![
|
|
is_first_run, create_vault, unlock, is_unlocked,
|
|
list_connections, create_connection, update_connection, delete_connection,
|
|
list_groups, create_group, delete_group, rename_group,
|
|
search_connections,
|
|
list_credentials, create_password, create_ssh_key, delete_credential,
|
|
get_setting, set_setting,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
```
|
|
|
|
- [ ] **Step 3:** Migrate `UnlockLayout.vue` — replace `Call.ByName("...WraithApp.Unlock", password)` with `invoke("unlock", { password })`
|
|
|
|
- [ ] **Step 4:** Migrate `app.store.ts`, `connection.store.ts` — same pattern
|
|
|
|
- [ ] **Step 5:** Verify: app launches, vault creation works, connections CRUD works
|
|
|
|
- [ ] **Step 6:** Commit: `feat: Tauri commands + frontend vault/connection wiring`
|
|
|
|
---
|
|
|
|
### Task 1.6: Port Remaining UI Components
|
|
|
|
**Files:**
|
|
- Migrate: All Vue components from current wraith repo
|
|
- Focus: Components with zero Wails dependencies (direct copy)
|
|
- Then: Components with Wails dependencies (Call → invoke, Events → listen)
|
|
|
|
- [ ] **Step 1:** Direct-copy components (no Wails deps): `SidebarToggle.vue`, `SessionContainer.vue`, `TabBar.vue`, `CommandPalette.vue`, `ContextMenu.vue`, `TransferProgress.vue`, `HostKeyDialog.vue`
|
|
|
|
- [ ] **Step 2:** Migrate `ConnectionTree.vue` — `Call` → `invoke`
|
|
- [ ] **Step 3:** Migrate `ConnectionEditDialog.vue` — `Call` → `invoke`
|
|
- [ ] **Step 4:** Migrate `SettingsModal.vue` — `Call` → `invoke`, `Browser.OpenURL` → `open` from `@tauri-apps/plugin-shell`
|
|
- [ ] **Step 5:** Migrate `ThemePicker.vue` — `Call` → `invoke`
|
|
- [ ] **Step 6:** Migrate `MainLayout.vue` — `Call` → `invoke`, `Application.Quit()` → `getCurrentWindow().close()`
|
|
- [ ] **Step 7:** Migrate `StatusBar.vue` — `Call` → `invoke`
|
|
|
|
- [ ] **Step 8:** Verify: full UI renders, sidebar works, connection CRUD works, settings persist
|
|
|
|
- [ ] **Step 9:** Commit: `feat: port all UI components to Tauri v2`
|
|
|
|
---
|
|
|
|
## Phase 2: SSH Terminal
|
|
|
|
### Task 2.1: Rust SSH Service
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/ssh/mod.rs`
|
|
- Create: `src-tauri/src/ssh/session.rs`
|
|
- Create: `src-tauri/src/ssh/host_key.rs`
|
|
|
|
- [ ] **Step 1:** Implement `SshService`:
|
|
- `connect(hostname, port, username, auth_methods, cols, rows) -> session_id`
|
|
- `write(session_id, data: &[u8])` — write to PTY stdin
|
|
- `resize(session_id, cols, rows)` — window change request
|
|
- `disconnect(session_id)`
|
|
- Uses `russh` async client with `tokio` runtime
|
|
- PTY request with `xterm-256color`, 115200 baud
|
|
- Stdout read loop emits `Tauri::Event("ssh:data:{session_id}", base64_data)`
|
|
|
|
- [ ] **Step 2:** Implement host key verification (TOFU):
|
|
- `HostKeyStore` backed by SQLite `host_keys` table
|
|
- New keys → store and accept
|
|
- Matching keys → accept silently
|
|
- Changed keys → reject with MITM warning
|
|
- Emit event to frontend for UI confirmation on new keys (optional enhancement)
|
|
|
|
- [ ] **Step 3:** Implement SFTP client creation on same SSH connection:
|
|
- After SSH connect, open SFTP subsystem channel
|
|
- Store SFTP client alongside SSH session
|
|
|
|
- [ ] **Step 4:** Write Tauri commands: `connect_ssh`, `connect_ssh_with_password`, `disconnect_session`, `ssh_write`, `ssh_resize`
|
|
|
|
- [ ] **Step 5:** Write tests — connect to localhost (if available), mock SSH server for unit tests
|
|
|
|
- [ ] **Step 6:** Commit: `feat: Rust SSH service with TOFU host key verification`
|
|
|
|
---
|
|
|
|
### Task 2.2: Terminal Frontend (xterm.js + Tauri Events)
|
|
|
|
**Files:**
|
|
- Rewrite: `src/composables/useTerminal.ts`
|
|
- Migrate: `src/components/terminal/TerminalView.vue`
|
|
- Rewrite: `src/stores/session.store.ts`
|
|
|
|
- [ ] **Step 1:** Rewrite `useTerminal.ts`:
|
|
- Replace `Events.On("ssh:data:...")` with `listen("ssh:data:{sessionId}", ...)`
|
|
- Replace `Call.ByName("...SSHService.Write", ...)` with `invoke("ssh_write", { sessionId, data })`
|
|
- Keep: streaming TextDecoder with `{ stream: true }`
|
|
- Keep: rAF write batching
|
|
- Keep: select-to-copy, right-click-to-paste
|
|
- **Fix:** `document.fonts.ready.then(() => fitAddon.fit())` — wait for fonts before measuring
|
|
- **Fix:** Font stack prioritizes platform-native fonts: `'Cascadia Mono', Consolas, 'SF Mono', Menlo, 'Courier New', monospace`
|
|
- **Fix:** After any `fitAddon.fit()`, call `terminal.scrollToBottom()` to prevent scroll jump
|
|
|
|
- [ ] **Step 2:** Rewrite `session.store.ts`:
|
|
- Replace `Call.ByName("...WraithApp.ConnectSSH", ...)` with `invoke("connect_ssh", { connectionId, cols, rows })`
|
|
- Keep: multi-session support, disambiguated names, theme propagation
|
|
|
|
- [ ] **Step 3:** Migrate `TerminalView.vue` — should work with only composable changes
|
|
|
|
- [ ] **Step 4:** End-to-end test: connect to a real SSH server, type commands, see output, resize terminal
|
|
|
|
- [ ] **Step 5:** Commit: `feat: xterm.js terminal with Tauri event bridge`
|
|
|
|
---
|
|
|
|
### Task 2.3: Terminal Theming
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/theme/mod.rs`
|
|
- Migrate: `src/components/common/ThemePicker.vue`
|
|
|
|
- [ ] **Step 1:** Implement theme service in Rust — same 7 built-in themes (Dracula, Nord, Monokai, One Dark, Solarized Dark, Gruvbox Dark, MobaXTerm Classic), seed on startup
|
|
|
|
- [ ] **Step 2:** Write Tauri commands: `list_themes`, `get_theme_by_name`
|
|
|
|
- [ ] **Step 3:** Wire `ThemePicker.vue` → `session.store.setTheme()` → `TerminalView.vue` watcher applies to `terminal.options.theme`
|
|
|
|
- [ ] **Step 4:** Commit: `feat: terminal theming with 7 built-in themes`
|
|
|
|
---
|
|
|
|
## Phase 3: SFTP
|
|
|
|
### Task 3.1: Rust SFTP Service
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/sftp/mod.rs`
|
|
|
|
- [ ] **Step 1:** Implement SFTP operations using `russh-sftp`:
|
|
- `list(session_id, path) -> Vec<FileEntry>`
|
|
- `read_file(session_id, path) -> String` (5MB guard)
|
|
- `write_file(session_id, path, content)`
|
|
- `mkdir(session_id, path)`
|
|
- `delete(session_id, path)` — handles file vs directory
|
|
- `rename(session_id, old_path, new_path)`
|
|
- `stat(session_id, path) -> FileEntry`
|
|
|
|
- [ ] **Step 2:** Write Tauri commands for each operation
|
|
|
|
- [ ] **Step 3:** Commit: `feat: SFTP operations service`
|
|
|
|
---
|
|
|
|
### Task 3.2: SFTP Frontend
|
|
|
|
**Files:**
|
|
- Rewrite: `src/composables/useSftp.ts`
|
|
- Migrate: `src/components/sftp/FileTree.vue`
|
|
|
|
- [ ] **Step 1:** Rewrite `useSftp.ts` — `invoke()` instead of `Call.ByName()`
|
|
|
|
- [ ] **Step 2:** Ensure FileTree toolbar buttons work: upload, download, delete, mkdir, refresh
|
|
|
|
- [ ] **Step 3:** Commit: `feat: SFTP sidebar with full file operations`
|
|
|
|
---
|
|
|
|
### Task 3.3: CWD Following (Separate Exec Channel)
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/ssh/cwd.rs`
|
|
|
|
- [ ] **Step 1:** Implement CWD tracker via **separate SSH exec channel** — NOT in the terminal data stream:
|
|
- On SSH connect, open a second channel via `session.channel_open_session()`
|
|
- Start a background task that runs `pwd` on this channel every 2 seconds
|
|
- When the output changes, emit `Tauri::Event("ssh:cwd:{session_id}", new_path)`
|
|
- The terminal data stream is NEVER touched
|
|
|
|
- [ ] **Step 2:** In `useSftp.ts`, listen for `ssh:cwd:{sessionId}` events. When `followTerminal` is enabled and path changes, call `navigateTo(newPath)`.
|
|
|
|
- [ ] **Step 3:** Test: `cd /etc` in terminal → SFTP sidebar navigates to `/etc` within 2 seconds
|
|
|
|
- [ ] **Step 4:** Commit: `feat: CWD following via separate SSH exec channel`
|
|
|
|
---
|
|
|
|
## Phase 4: RDP
|
|
|
|
### Task 4.1: Rust RDP Service
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/rdp/mod.rs`
|
|
- Create: `src-tauri/src/rdp/input.rs`
|
|
|
|
- [ ] **Step 1:** Evaluate RDP options:
|
|
- **Option A:** `ironrdp` (pure Rust, by Devolutions) — preferred if mature enough
|
|
- **Option B:** FreeRDP3 FFI via `libloading` — same approach as current Go purego, but Rust FFI
|
|
- Pick based on which can deliver 1080p @ 30fps bitmap updates
|
|
|
|
- [ ] **Step 2:** Implement RDP service:
|
|
- `connect(config) -> session_id`
|
|
- `send_mouse(session_id, x, y, flags)`
|
|
- `send_key(session_id, scancode, pressed)`
|
|
- `send_clipboard(session_id, text)`
|
|
- `disconnect(session_id)`
|
|
- **Frame delivery via Tauri asset protocol** (not base64 IPC):
|
|
Register a custom protocol handler in `main.rs` that serves raw RGBA frames:
|
|
```rust
|
|
.register_assetprotocol("rdpframe", move |_app, request| {
|
|
// Parse session_id from URL path
|
|
// Return frame bytes as image/raw with correct dimensions header
|
|
})
|
|
```
|
|
Frontend fetches `asset://localhost/rdpframe/{session_id}` via `requestAnimationFrame` loop.
|
|
Eliminates base64 encode/decode overhead for 1080p frames (~8MB/frame raw).
|
|
|
|
- [ ] **Step 3:** Port scancode mapping table from current `internal/rdp/input.go`
|
|
|
|
- [ ] **Step 4:** Write Tauri commands for all RDP operations
|
|
|
|
- [ ] **Step 5:** Commit: `feat: RDP service`
|
|
|
|
---
|
|
|
|
### Task 4.2: RDP Frontend
|
|
|
|
**Files:**
|
|
- Rewrite: `src/composables/useRdp.ts`
|
|
- Migrate: `src/components/rdp/RdpView.vue`
|
|
|
|
- [ ] **Step 1:** Rewrite `useRdp.ts` — canvas rendering of RGBA frames from `invoke("rdp_get_frame")`
|
|
|
|
- [ ] **Step 2:** Wire mouse/keyboard capture → `invoke("rdp_send_mouse")` / `invoke("rdp_send_key")`
|
|
|
|
- [ ] **Step 3:** Commit: `feat: RDP canvas rendering in tabs`
|
|
|
|
---
|
|
|
|
## Phase 5: Polish
|
|
|
|
### Task 5.1: Tab Detach/Reattach
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/session/mod.rs`
|
|
- Modify: `src/components/session/TabBar.vue`
|
|
|
|
- [ ] **Step 1:** Implement session manager in Rust using `DashMap` for lock-free concurrent access:
|
|
```rust
|
|
use dashmap::DashMap;
|
|
|
|
pub struct SessionManager {
|
|
sessions: DashMap<String, Session>,
|
|
}
|
|
|
|
// Session implements Drop — SSH/SFTP connections close automatically
|
|
// when the session is removed from the map or the manager is dropped.
|
|
impl Drop for Session {
|
|
fn drop(&mut self) {
|
|
// Close SSH client, SFTP client, RDP backend
|
|
// No zombie connections possible — Rust ownership guarantees it
|
|
}
|
|
}
|
|
```
|
|
- `detach(session_id)` — creates new Tauri `WebviewWindow` pointing at same session
|
|
- `reattach(session_id)` — destroys detached window, re-renders in main tab bar
|
|
|
|
- [ ] **Step 2:** Use Tauri's `WebviewWindow::new()` for detached windows
|
|
|
|
- [ ] **Step 3:** Add detach icon (pop-out arrow) to TabBar, "Session detached — Reattach" placeholder in original tab
|
|
|
|
- [ ] **Step 4:** Commit: `feat: tab detach/reattach via Tauri multi-window`
|
|
|
|
---
|
|
|
|
### Task 5.2: CodeMirror Editor (Separate Window)
|
|
|
|
**Files:**
|
|
- Modify: `src/components/editor/EditorWindow.vue`
|
|
- Create: `src/editor.html` — dedicated entry point for editor windows
|
|
|
|
- [ ] **Step 1:** Create a separate Tauri window for the editor:
|
|
- Rust side: `WebviewWindow::new()` with URL pointing to `/editor?session={id}&path={path}`
|
|
- Vue side: `editor.html` entry point that renders only the CodeMirror component
|
|
- Window title: `filename — host — Wraith Editor`
|
|
|
|
- [ ] **Step 2:** Editor reads file via `invoke("sftp_read_file")`, saves via `invoke("sftp_write_file")`
|
|
|
|
- [ ] **Step 3:** File size guard: refuse files >5MB, offer download instead
|
|
|
|
- [ ] **Step 4:** Commit: `feat: CodeMirror editor in separate window`
|
|
|
|
---
|
|
|
|
### Task 5.3: Keyboard Shortcuts
|
|
|
|
**Files:**
|
|
- Modify: `src/layouts/MainLayout.vue`
|
|
|
|
- [ ] **Step 1:** Implement full shortcut set from spec:
|
|
|
|
| Shortcut | Action |
|
|
|---|---|
|
|
| Ctrl+K | Command palette |
|
|
| Ctrl+T | New SSH session (quick connect) |
|
|
| Ctrl+W | Close current tab |
|
|
| Ctrl+Tab | Next tab |
|
|
| Ctrl+Shift+Tab | Previous tab |
|
|
| Ctrl+1-9 | Switch to tab N |
|
|
| Ctrl+B | Toggle sidebar |
|
|
| Ctrl+Shift+D | Detach current tab |
|
|
| F11 | Fullscreen |
|
|
| Ctrl+F | Search in terminal scrollback |
|
|
|
|
- [ ] **Step 2:** Commit: `feat: keyboard shortcuts`
|
|
|
|
---
|
|
|
|
### Task 5.4: Tab Badges + Status Bar
|
|
|
|
**Files:**
|
|
- Create: `src/components/session/TabBadge.vue`
|
|
- Modify: `src/components/common/StatusBar.vue`
|
|
|
|
- [ ] **Step 1:** Tab badges: protocol icon (SSH green / RDP blue), ROOT warning (check username), environment pills from connection tags (PROD/DEV/STAGING)
|
|
|
|
- [ ] **Step 2:** Status bar: live terminal dimensions from `onResize`, protocol + user@host:port, theme name, encoding
|
|
|
|
- [ ] **Step 3:** Commit: `feat: tab badges + live status bar`
|
|
|
|
---
|
|
|
|
### Task 5.5: Auto-Updater
|
|
|
|
**Files:**
|
|
- Modify: `src-tauri/tauri.conf.json`
|
|
- Modify: `src/components/common/SettingsModal.vue`
|
|
|
|
- [ ] **Step 1:** Configure Tauri built-in updater in `tauri.conf.json`:
|
|
```json
|
|
{
|
|
"plugins": {
|
|
"updater": {
|
|
"endpoints": ["https://git.command.vigilcyber.com/api/v1/repos/vstockwell/wraith-v2/releases/latest"],
|
|
"pubkey": "..."
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2:** Wire Settings "Check for Updates" button to `@tauri-apps/plugin-updater` API
|
|
|
|
- [ ] **Step 3:** Update CI workflow for Tauri bundler + code signing
|
|
|
|
- [ ] **Step 4:** Commit: `feat: Tauri auto-updater with code signing`
|
|
|
|
---
|
|
|
|
### Task 5.6: Workspace Restore (Crash Recovery)
|
|
|
|
**Files:**
|
|
- Create: `src-tauri/src/workspace/mod.rs`
|
|
|
|
- [ ] **Step 1:** Port workspace service from Go:
|
|
- Save tab layout to settings as JSON every 30 seconds + on session open/close
|
|
- Mark clean shutdown on exit
|
|
- On startup, detect dirty shutdown → offer to restore tab layout
|
|
|
|
- [ ] **Step 2:** Commit: `feat: workspace crash recovery`
|
|
|
|
---
|
|
|
|
## Phase 6: CI/CD + Distribution
|
|
|
|
### Task 6.1: Gitea Actions Workflow
|
|
|
|
**Files:**
|
|
- Create: `.gitea/workflows/build-release.yml`
|
|
|
|
- [ ] **Step 1:** Build workflow:
|
|
- Trigger on `v*` tags
|
|
- `cargo tauri build` for Windows (cross-compile from Linux or native Windows runner)
|
|
- Code signing via Azure Key Vault (jsign, same as current)
|
|
- Create Gitea Release with `tag_name: "v${VERSION}"` (include the v prefix!)
|
|
- Upload installer to Gitea packages
|
|
|
|
- [ ] **Step 2:** Test: push a tag, verify installer downloads and auto-updater finds it
|
|
|
|
- [ ] **Step 3:** Commit: `feat: CI/CD pipeline for Tauri builds`
|
|
|
|
---
|
|
|
|
## Phase 7: Go Codebase Cleanup
|
|
|
|
### Task 7.1: Archive Go Repository
|
|
|
|
- [ ] **Step 1:** Verify Tauri version is stable and deployed to production
|
|
- [ ] **Step 2:** Rename `wraith` repo to `wraith-go-legacy` on Gitea
|
|
- [ ] **Step 3:** Rename `wraith-v2` to `wraith`
|
|
- [ ] **Step 4:** Update auto-updater endpoint to new repo
|
|
- [ ] **Step 5:** Delete old Gitea packages (Go-built versions)
|
|
- [ ] **Step 6:** Archive the legacy repo (read-only)
|
|
- [ ] **Step 7:** Update any documentation, CLAUDE.md references
|
|
|
|
---
|
|
|
|
## Migration Notes
|
|
|
|
### Database Compatibility
|
|
|
|
The SQLite schema is identical between Go and Rust versions. The Commander can copy `%APPDATA%\Wraith\wraith.db` from the Go version and the Rust version will read it — connections, credentials, host keys, settings, themes all carry over.
|
|
|
|
**Critical:** The vault encryption format (`v1:{iv_hex}:{sealed_hex}`) must be byte-compatible between Go's `crypto/aes` + `crypto/cipher` and Rust's `aes-gcm` crate. Test this in Task 1.3 Step 3.
|
|
|
|
### What Gets Deleted (Copilot/AI)
|
|
|
|
The Go codebase has an AI copilot integration (8 files in `internal/ai/`). This is NOT being ported. The Commander will use Claude Code over SSH instead. Delete these from scope:
|
|
- `internal/ai/` — entire package
|
|
- `frontend/src/composables/useCopilot.ts`
|
|
- `frontend/src/stores/copilot.store.ts`
|
|
- `frontend/src/components/copilot/` — entire directory
|
|
|
|
### Wails → Tauri Migration Cheatsheet
|
|
|
|
| Wails v3 | Tauri v2 |
|
|
|---|---|
|
|
| `Call.ByName("full.go.path.Method", args)` | `invoke("method_name", { args })` |
|
|
| `Events.On("event:name", callback)` | `listen("event:name", callback)` |
|
|
| `Events.Emit("event:name", data)` | `emit("event:name", data)` (Rust side) |
|
|
| `Application.Quit()` | `getCurrentWindow().close()` |
|
|
| `Browser.OpenURL(url)` | `open(url)` from `@tauri-apps/plugin-shell` |
|
|
| Go struct → JSON → JS object | Rust struct (serde) → JSON → TS interface |
|
|
| `application.NewWebviewWindow()` | `WebviewWindow::new()` |
|
|
|
|
---
|
|
|
|
## Risk Register
|
|
|
|
| Risk | Mitigation |
|
|
|---|---|
|
|
| `russh` async complexity | Use `tokio` throughout; russh is well-documented with examples |
|
|
| ironrdp maturity | Fallback to FreeRDP FFI if ironrdp can't deliver 1080p@30fps |
|
|
| Tauri v2 multi-window edge cases | Spike tab detach early in Phase 5; fall back to floating panels |
|
|
| Vault encryption compatibility | Test Go↔Rust encryption in Phase 1 before building anything else |
|
|
| Windows code signing in Tauri | Same jsign + Azure Key Vault approach; Tauri bundler produces .exe |
|
|
| Cross-platform SSH crate differences | russh is pure Rust, no platform-specific code; test on Windows early |
|