Full codebase audit against the 983-line design spec. Documents what works, what's implemented but unwired, what's missing, bugs found and fixed, unused dependencies, and recommended priority fix order. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
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.Managerandssh.SSHServiceboth track sessions independentlyHostKeyStoreandSSHService.Connect()both exist but aren't connectedCWDTrackerand the SFTP sidebar both exist but aren't connectedWorkspaceServiceexists but nothing calls itplugin.Registryexists 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
@clickhandlers - "Follow terminal folder" checkbox bound to a ref that nothing updates
TransferProgress.vuethat never receives transfer dataHostKeyDialog.vuethat'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)
- SFTP toolbar buttons — wire upload/download/delete/mkdir. The Go
SFTPServicealready has all these methods. Just needs@clickhandlers inFileTree.vue. - Host key verification — wire
HostKeyStoreintoSSHService.Connect(). Security gap. - CWD following — wire
CWDTrackerinto the SSH read loop. This is MobaXTerm's killer differentiator. - Theme application — pass selected theme to
terminal.options.theme. One line of code. - Session Manager integration — call
sessionMgr.Create()on connect. Prerequisite for tab detach. - Editor in separate window — requires Go-side
application.NewWebviewWindow()+ dedicated route. - Keyboard shortcuts — straightforward
handleKeydownadditions inMainLayout.vue. - Workspace restore — wire
WorkspaceServiceinto app lifecycle.