wraith/docs/superpowers/plans/2026-03-17-tauri-v2-rewrite.md
Vantz Stockwell 5de73dfabb docs: Tauri v2 rewrite plan — full MobaXTerm replacement
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>
2026-03-17 14:48:22 -04:00

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), 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

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 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:

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:

#[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 — 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.vueCallinvoke

  • Step 3: Migrate ConnectionEditDialog.vueCallinvoke

  • Step 4: Migrate SettingsModal.vueCallinvoke, Browser.OpenURLopen from @tauri-apps/plugin-shell

  • Step 5: Migrate ThemePicker.vueCallinvoke

  • Step 6: Migrate MainLayout.vueCallinvoke, Application.Quit()getCurrentWindow().close()

  • Step 7: Migrate StatusBar.vueCallinvoke

  • 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.vuesession.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.tsinvoke() 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:
      .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:

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:

{
  "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