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>
31 KiB
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),ironrdpor 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):
DashMapfor session registry — lock-free concurrent hashmap instead ofRwLock<HashMap>. No deadlock risk during multi-window tab detach/reattach transitions.Droptrait 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 vianpm create tauri-app -
Create:
src-tauri/tauri.conf.json -
Create:
src-tauri/Cargo.toml -
Step 1: Create new project with Tauri v2 CLI
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:
[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 devlaunches 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:
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 devshows 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 ONpub fn migrate(conn: &Connection) -> Result<()>— runs embedded SQL migrations- Use
rusqlite::Connectionwrapped inArc<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=4generate_salt() -> [u8; 32]— 32 random bytes viarandencrypt(key: &[u8; 32], plaintext: &str) -> String— AES-256-GCM, random 12-byte IV, returnsv1:{iv_hex}:{sealed_hex}decrypt(key: &[u8; 32], blob: &str) -> Result<String>— parses v1 format, decryptsVaultServicestruct 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 viajson_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:
#[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:
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— replaceCall.ByName("...WraithApp.Unlock", password)withinvoke("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→openfrom@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_idwrite(session_id, data: &[u8])— write to PTY stdinresize(session_id, cols, rows)— window change requestdisconnect(session_id)- Uses
russhasync client withtokioruntime - 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):
HostKeyStorebacked by SQLitehost_keystable- 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:...")withlisten("ssh:data:{sessionId}", ...) - Replace
Call.ByName("...SSHService.Write", ...)withinvoke("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(), callterminal.scrollToBottom()to prevent scroll jump
- Replace
-
Step 2: Rewrite
session.store.ts:- Replace
Call.ByName("...WraithApp.ConnectSSH", ...)withinvoke("connect_ssh", { connectionId, cols, rows }) - Keep: multi-session support, disambiguated names, theme propagation
- Replace
-
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.vuewatcher applies toterminal.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 directoryrename(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 ofCall.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
pwdon 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
- On SSH connect, open a second channel via
-
Step 2: In
useSftp.ts, listen forssh:cwd:{sessionId}events. WhenfollowTerminalis enabled and path changes, callnavigateTo(newPath). -
Step 3: Test:
cd /etcin terminal → SFTP sidebar navigates to/etcwithin 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
- Option A:
-
Step 2: Implement RDP service:
connect(config) -> session_idsend_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.rsthat serves raw RGBA frames:
Frontend fetches.register_assetprotocol("rdpframe", move |_app, request| { // Parse session_id from URL path // Return frame bytes as image/raw with correct dimensions header })asset://localhost/rdpframe/{session_id}viarequestAnimationFrameloop. 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 frominvoke("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
DashMapfor lock-free concurrent access:
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 TauriWebviewWindowpointing 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.htmlentry point that renders only the CodeMirror component - Window title:
filename — host — Wraith Editor
- Rust side:
-
Step 2: Editor reads file via
invoke("sftp_read_file"), saves viainvoke("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:
{
"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-updaterAPI -
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 buildfor 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
- Trigger on
-
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
wraithrepo towraith-go-legacyon Gitea - Step 3: Rename
wraith-v2towraith - 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 packagefrontend/src/composables/useCopilot.tsfrontend/src/stores/copilot.store.tsfrontend/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 |