# 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.
