# Wraith Desktop — Fired XO Audit > **Date:** 2026-03-17 > **Auditor:** VHQ XO (Claude Opus 4.6, assuming the con) > **Spec:** `docs/superpowers/specs/2026-03-17-wraith-desktop-design.md` (983 lines, 16 sections) > **Codebase:** 20 commits at time of audit, two prior XOs contributed --- ## Executive Summary The spec describes a MobaXTerm replacement with multi-tabbed SSH terminals, SFTP sidebar with full file operations, RDP via FreeRDP3, encrypted vault, terminal theming, command palette, tab detach/reattach, CWD following, and workspace crash recovery. What was delivered is a skeleton with functional SSH terminals and a partially-wired SFTP sidebar. The Go backend is deceptively complete at the code level (~75%), but critical services are implemented and never connected to each other. The frontend tells the real story: 4 of 7 SFTP toolbar buttons do nothing, themes don't apply, tab features are stubs, and the editor opens as a 300px inline panel instead of a separate window. **User-facing functionality: ~40% of spec. Code-level completeness: ~65%.** --- ## Bugs Fixed During This Audit ### BUG-001: UTF-8 Terminal Rendering (v0.7.1) **Root cause:** `atob()` decodes base64 but returns a Latin-1 "binary string" where each byte is a separate character code. Multi-byte UTF-8 sequences (box-drawing chars, em dashes, arrows) were split into separate Latin-1 codepoints, producing mojibake. **Fix:** Reconstruct raw byte array from `atob()` output, decode via `TextDecoder('utf-8')`. **File:** `frontend/src/composables/useTerminal.ts` line 117 ### BUG-002: Split UTF-8 Across Chunk Boundaries (v0.7.2) **Root cause:** The v0.7.1 fix created a `new TextDecoder()` on every Wails event. The Go read loop uses a 32KB buffer — multi-byte UTF-8 characters split across two `reader.Read()` calls produced two separate events. Each half was decoded independently, still producing mojibake for characters that happened to land on a chunk boundary. **Fix:** Single `TextDecoder` instance with `{ stream: true }` persisted for the session lifetime. Buffers incomplete multi-byte sequences between events. **File:** `frontend/src/composables/useTerminal.ts` ### BUG-003: Typewriter Lag (v0.7.2) **Root cause:** Every Go `reader.Read()` (sometimes single bytes for SSH PTY output) triggered a separate Wails event emission, which triggered a separate `terminal.write()`. The serialization round-trip (base64 encode -> Wails event -> JS listener -> base64 decode -> TextDecoder -> xterm.js write) for every few bytes produced visible line-by-line lag. **Fix:** `requestAnimationFrame` write batching. Incoming data accumulates in a string buffer and flushes to xterm.js once per animation frame (~16ms). All chunks within a frame render in a single write. **File:** `frontend/src/composables/useTerminal.ts` ### BUG-004: PTY Baud Rate (v0.7.2) **Root cause:** `TTY_OP_ISPEED` and `TTY_OP_OSPEED` set to 14400 (1995 modem speed). Some remote PTYs throttle output to match the declared baud rate. **Fix:** Bumped to 115200. **File:** `internal/ssh/service.go` line 76-77 --- ## What Actually Works (End-to-End) | Feature | Status | |---|---| | Vault (master password, Argon2id, AES-256-GCM) | Working | | Connection manager (groups, tree, CRUD, search) | Working | | SSH terminal (multi-tab, xterm.js, resize) | Working (fixed in v0.7.1/v0.7.2) | | SFTP directory listing + navigation | Working | | SFTP file read/write via CodeMirror editor | Working (but editor is inline, not separate window) | | MobaXTerm .mobaconf import | Working | | Credential management (passwords + SSH keys) | Working | | Auto-updater (Gitea releases, SHA256 verify) | Working | | Command palette (Ctrl+K) | Partial — searches connections, "Open Vault" is TODO | | Quick connect (toolbar) | Working | | Context menu (right-click connections) | Working | | Status bar | Partial — terminal dimensions hardcoded "120x40" | | 7 built-in terminal themes | Selectable but don't apply to xterm.js | --- ## Implemented in Go But Never Wired These are fully implemented backend services sitting disconnected from the rest of the application: ### Host Key Verification — SECURITY GAP `internal/ssh/hostkey.go` — `HostKeyStore` with `Verify()`, `Store()`, `Delete()`, `GetFingerprint()`. All real logic. **Problem:** SSH service hardcodes `ssh.InsecureIgnoreHostKey()` at `service.go:58`. The `HostKeyStore` is never instantiated or called. Every connection silently accepts any server key. MITM attacks would succeed without warning. **Frontend:** `HostKeyDialog.vue` exists with accept/reject UI and MITM warning text. Also never wired. ### CWD Tracking (SFTP "Follow Terminal Folder") `internal/ssh/cwd.go` — Full OSC 7 escape sequence parser. `ProcessOutput()` scans SSH stdout for `\033]7;file://hostname/path\033\\`, strips them before forwarding to xterm.js, and emits CWD change events. Shell hook generator for bash/zsh/fish. **Problem:** `CWDTracker` is never instantiated in `SSHService` or wired into the read loop. The SFTP sidebar's "Follow terminal folder" checkbox renders but nothing pushes CWD changes from the terminal. ### Session Manager `internal/session/manager.go` — `Create()`, `Detach()`, `Reattach()`, `SetState()`, max 32 sessions enforcement. All real logic. **Problem:** Instantiated in `app.go` but `Create()` is never called when SSH/RDP sessions open. The SSH service has its own internal session map. These two tracking systems are parallel and unconnected. Tab detach/reattach (which depends on the Session Manager) cannot work. ### Workspace Restore (Crash Recovery) `internal/app/workspace.go` — `WorkspaceService` with `Save()`, `Load()`, `MarkCleanShutdown()`, `WasCleanShutdown()`, `ClearCleanShutdown()`. All real logic via settings persistence. **Problem:** `WorkspaceService` is never instantiated in `app.go`. Dead code. Crash recovery does not exist. ### Plugin Registry `internal/plugin/registry.go` — `RegisterProtocol()`, `RegisterImporter()`, `GetProtocol()`, `GetImporter()`. All real logic. **Problem:** Instantiated in `app.go`, nothing is ever registered. The MobaXTerm importer bypasses the registry entirely (called directly). Empty scaffolding. --- ## Missing Features (Spec vs. Reality) ### Phase 2 (SSH + SFTP) — Should Be Done | Feature | Status | Notes | |---|---|---| | SFTP upload | Button rendered, no click handler | `FileTree.vue` toolbar | | SFTP download | Button rendered, no click handler | `FileTree.vue` toolbar | | SFTP delete | Button rendered, no click handler | `FileTree.vue` toolbar | | SFTP new folder | Button rendered, no click handler | `FileTree.vue` toolbar | | SFTP CWD following | Checkbox rendered, does nothing | Go code exists but unwired | | Transfer progress | UI panel exists, always empty | `TransferProgress.vue` — decorative | | CodeMirror in separate window | Opens as 300px inline panel | Needs `application.NewWebviewWindow()` | | Multiple connections to same host | Not working | Session store may block duplicate connections | ### Phase 3 (RDP) | Feature | Status | Notes | |---|---|---| | RDP via FreeRDP3 | Code complete on Go side | Windows-only, mock backend on other platforms | | RDP clipboard sync | `SendClipboard()` is `return nil` stub | `freerdp_windows.go` — TODO comment | ### Phase 4 (Polish) — Mostly Missing | Feature | Status | |---|---| | Tab detach/reattach | Not implemented (Session Manager unwired) | | Tab drag-to-reorder | Not implemented | | Tab badges (PROD/ROOT/DEV) | `isRootUser()` hardcoded `false` | | Theme application | ThemePicker selects, persists name, but never passes theme to xterm.js | | Per-connection theme | Not implemented | | Custom theme creation | Not implemented | | Vault management UI | No standalone UI; keys/passwords created inline in ConnectionEditDialog | | Host key management UI | `HostKeyDialog.vue` exists, not wired | | Keyboard shortcuts | Only Ctrl+K works. No Ctrl+T/W/Tab/1-9/B/Shift+D/F11/F | | Connection history | `connection_history` table not in migrations | | First-run .mobaconf auto-detect | Manual import only | | Workspace crash recovery | Go code exists, never wired | --- ## Unused Dependencies | Package | Declared In | Actually Used | |---|---|---| | `naive-ui` | `package.json` | Zero components imported anywhere — entire UI is hand-rolled Tailwind | | `@xterm/addon-webgl` | `package.json` | Never imported — FitAddon/SearchAddon/WebLinksAddon are used | --- ## Phase Completion Assessment | Phase | Scope | Completion | Notes | |---|---|---|---| | **1: Foundation** | Vault, connections, SQLite, themes, scaffold | ~90% | Plugin registry is empty scaffolding | | **2: SSH + SFTP** | SSH terminal, SFTP sidebar, CWD, CodeMirror | ~40% | SSH works; SFTP browse/read works; upload/download/delete/mkdir/CWD all missing | | **3: RDP** | FreeRDP3, canvas, input, clipboard | ~70% | Code is real and impressive; clipboard is a no-op | | **4: Polish** | Command palette, detach, themes, import, shortcuts | ~15% | Command palette partial; everything else missing or cosmetic | --- ## Architectural Observations ### The "Parallel Systems" Problem The codebase has a recurring pattern: a service is fully implemented in isolation but never integrated with the services it depends on or that depend on it. - `session.Manager` and `ssh.SSHService` both track sessions independently - `HostKeyStore` and `SSHService.Connect()` both exist but aren't connected - `CWDTracker` and the SFTP sidebar both exist but aren't connected - `WorkspaceService` exists but nothing calls it - `plugin.Registry` exists but nothing registers with it This suggests the XOs worked on individual packages in isolation without doing the integration pass that connects them into a working system. ### The "Rendered But Not Wired" Problem The frontend has multiple UI elements that look functional but have no behavior: - 4 SFTP toolbar buttons with no `@click` handlers - "Follow terminal folder" checkbox bound to a ref that nothing updates - `TransferProgress.vue` that never receives transfer data - `HostKeyDialog.vue` that's never shown - Theme selection that persists a name but doesn't change terminal colors This creates a deceptive impression of completeness when demoing the app. ### What Was Done Well - **Vault encryption** is properly implemented (Argon2id + AES-256-GCM with versioned format) - **FreeRDP3 purego bindings** are genuinely impressive — full DLL integration without CGo - **SSH service** core is solid (after the encoding fixes) - **Connection manager** with groups, search, and tag filtering works well - **MobaXTerm importer** correctly parses the .mobaconf format - **Auto-updater** with SHA256 verification is production-ready --- ## Priority Fix Order (Recommended) 1. **SFTP toolbar buttons** — wire upload/download/delete/mkdir. The Go `SFTPService` already has all these methods. Just needs `@click` handlers in `FileTree.vue`. 2. **Host key verification** — wire `HostKeyStore` into `SSHService.Connect()`. Security gap. 3. **CWD following** — wire `CWDTracker` into the SSH read loop. This is MobaXTerm's killer differentiator. 4. **Theme application** — pass selected theme to `terminal.options.theme`. One line of code. 5. **Session Manager integration** — call `sessionMgr.Create()` on connect. Prerequisite for tab detach. 6. **Editor in separate window** — requires Go-side `application.NewWebviewWindow()` + dedicated route. 7. **Keyboard shortcuts** — straightforward `handleKeydown` additions in `MainLayout.vue`. 8. **Workspace restore** — wire `WorkspaceService` into app lifecycle.