Debug logging:
- wraith_log!() macro available in all modules, writes to wraith.log
- SSH connect/auth, PTY spawn, RDP connect all log with session IDs
- MCP startup panic now shows the actual error message
Copilot "Tools" button:
- Shows when a PTY session is active in the copilot panel
- Injects a formatted list of all 18 MCP tools into the chat
- Groups tools by category: session, terminal, SFTP, network, utilities
- Includes parameter signatures so the AI knows how to call them
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was logging "panicked — continuing" without the WHY. Now captures
the panic payload (String, &str, or type_id) so the log shows
exactly what went wrong in clone_services().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RDP: wrapped connection thread in catch_unwind so panics are logged
to wraith.log instead of silently killing the channel. Error message
now directs user to check the log.
CWD: changed cd . to cd ~ after OSC 7 hook injection so SFTP starts
at the user's home directory on macOS (where / requires explicit nav).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The printf argument had escaped quotes that passed through literally,
producing paths like /"/Users/foo". Removed the outer escaped quotes
— printf %s handles the command substitution directly. Also simplified
PROMPT_COMMAND assignment to avoid quote nesting issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CARGO_PKG_VERSION is always 0.1.0 (hardcoded in Cargo.toml). CI
patches tauri.conf.json from the git tag. Now reads app_handle.config()
for the real version so the update checker compares correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Checks Gitea releases API for latest version on startup. If newer
version available, shows confirm dialog to open download page.
Also adds "Check for Updates" button in Settings → About with
version comparison, release notes display, and download button.
Backend: check_for_updates command with semver comparison (6 tests).
96 total tests, zero warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/home doesn't exist on macOS (home dirs are /Users/). Changed default
SFTP path to / so it always loads. OSC 7 parser now strips stray
quotes from shell printf output that produced paths like /"/path".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switched to printf '\e]7;file://localhost/%s\a' with sed space encoding.
BEL (\a) terminator is more universally supported than ST (\e\\).
Shared __wraith_osc7 function avoids duplicating the printf across
bash/zsh branches.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
precmd/PROMPT_COMMAND only fire after a command runs. Without cd .
the first OSC 7 never emits and SFTP doesn't know the initial directory.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CWD hook command was visible to the user. Now wrapped in
stty -echo/echo to suppress echo, followed by clear to wipe the
screen. Space prefix prevents history recording in most shells.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Save Private Key and Save Public Key buttons download the generated
keys as id_ed25519/id_rsa (private) and .pub (public) files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Right-click → Edit now opens a separate Tauri window with the file
content in a monospace editor. Ctrl+S saves back to remote via SFTP.
Tab inserts 4 spaces. Modified indicator in toolbar.
Removed the inline EditorWindow overlay that covered the terminal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tauri v2 ACL requires explicit permissions to create windows from the
frontend. Added core🪟allow-create and
core:webview:allow-create-webview-window so the Tools menu can open
popup windows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
env_logger was never initialized so log::info/error went nowhere.
Added write_log() that appends to data_dir/wraith.log with timestamps.
Logs MCP server startup success/failure and any panics.
Check: %APPDATA%\Wraith\wraith.log (Windows)
~/Library/Application Support/Wraith/wraith.log (macOS)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SFTP tab switch fix:
- Removed :key on FileTree that destroyed component on every switch
- useSftp now accepts a reactive Ref<string> sessionId
- Watches sessionId changes and reinitializes without destroying state
- Per-session path memory via sessionPaths map — switching back to a
tab restores exactly where you were browsing
CWD following fix (macOS + all platforms):
- Injects OSC 7 prompt hook into the shell after SSH connect
- zsh: precmd() emits \e]7;file://host/path\e\\
- bash: PROMPT_COMMAND emits the same sequence
- Sent via the PTY channel so it configures the interactive shell
- The passive OSC 7 parser in the output loop picks it up
- SFTP sidebar auto-navigates to the current working directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was sending the command after a blind 300ms delay with \n — too early
for PowerShell startup banner, and \n caused a blank line before the
command.
Fix: poll mcp_terminal_read every 200ms until a prompt is detected
($, #, %, >, PS>), then send the command with \r (carriage return,
not newline). Falls back to sending after 5s timeout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The MCP server and error watcher are nice-to-have services that were
crashing the app on startup. Wrapped in catch_unwind + error handling
so a failure in these subsystems logs an error instead of taking down
the entire application.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Used getVersion() from @tauri-apps/api/app which reads the version
from tauri.conf.json (patched by CI from the git tag).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every tool in Wraith is now callable by the AI through MCP:
| MCP Tool | AI Use Case |
|-------------------|------------------------------------------|
| network_scan | "What devices are on this subnet?" |
| port_scan | "Which servers have SSH open?" |
| ping | "Is this host responding?" |
| traceroute | "Show me the route to this server" |
| dns_lookup | "What's the MX record for this domain?" |
| whois | "Who owns this IP?" |
| wake_on_lan | "Wake up the backup server" |
| bandwidth_test | "How fast is this server's internet?" |
| subnet_calc | "How many hosts in a /22?" |
| generate_ssh_key | "Generate an ed25519 key pair" |
| generate_password | "Generate a 32-char password" |
| terminal_read | "What's on screen right now?" |
| terminal_execute | "Run df -h on this server" |
| terminal_screenshot| "What's that RDP error?" |
| sftp_list/read/write| "Read the nginx config" |
| list_sessions | "What sessions are active?" |
11 new HTTP endpoints on the MCP server. 11 new tool definitions
in the bridge binary. The AI doesn't just chat — it scans, discovers,
analyzes, and connects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 new tools with full backend + popup UIs:
DNS Lookup:
- dig/nslookup/host fallback chain on remote host
- Record type selector (A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR)
Whois:
- Remote whois query, first 80 lines
- Works for domains and IP addresses
Bandwidth Test (2 modes):
- iperf3: LAN speed test between remote host and iperf server
- Internet: speedtest-cli / curl-based Cloudflare test fallback
Subnet Calculator:
- Pure Rust, no SSH needed
- CIDR input with quick-select buttons (/8 through /32)
- Displays: network, broadcast, netmask, wildcard, host range,
total/usable hosts, class, private/public
Tools menu now has 11 items across 3 sections.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 7 tool windows with full UIs:
- Network Scanner: subnet scan, ARP+DNS discovery, results table with
Quick Scan per host, SSH/RDP connect buttons, CSV export
- Port Scanner: custom range or quick scan (24 common ports), open/closed
results table with service names
- Ping: remote ping with count, raw output display
- Traceroute: remote traceroute, raw output display
- Wake on LAN: MAC address input, broadcasts via python3 on remote host
- SSH Key Generator: ed25519/RSA, copy public/private key, fingerprint
- Password Generator: configurable length/charset, copy button, history
Architecture:
- App.vue detects tool mode via URL hash (#/tool/name?sessionId=...)
- ToolWindow.vue routes to correct tool component
- Tools menu in toolbar opens Tauri popup windows (WebviewWindow)
- Capabilities grant tool-* windows the same permissions as main
- SFTP context menu: right-click Edit/Download/Rename/Delete
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tools menu in toolbar (next to File) with 7 tools:
- Network Scanner (existing scan_network command)
- Port Scanner (existing scan_ports/quick_scan commands)
- Ping — via SSH exec channel, cross-platform
- Traceroute — via SSH exec channel
- Wake on LAN — broadcasts WoL magic packet via python3 on remote host
- SSH Key Generator — pure Rust ed25519/RSA keygen via ssh-key crate
- Password Generator — cryptographic random with configurable charset
Backend: all 5 new Tauri commands (tool_ping, tool_traceroute,
tool_wake_on_lan, tool_generate_ssh_key, tool_generate_password)
Frontend: Tools dropdown menu wired, popup window launcher ready.
Tool window UIs (the actual panels inside each popup) to follow.
SFTP context menu: right-click Edit/Download/Rename/Delete working.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Network scanner (through SSH exec channels):
- scan_network: ping sweep + ARP table + reverse DNS on remote network
- scan_ports: TCP connect scan via bash /dev/tcp (parallel batches of 20)
- quick_scan: 24 common ports (SSH, HTTP, RDP, SMB, DB, etc.)
- Cross-platform: Linux + macOS
- No agent/nmap required — uses standard POSIX commands
- All scans run on the remote host through existing SSH tunnel
SFTP context menu:
- Right-click on files/folders shows Edit, Download, Rename, Delete
- Right-click on folders shows Open Folder
- Teleport menu to body for proper z-index layering
- Click-away handler to close menu
- Rename uses sftp_rename invoke
CI fix:
- Added default-run = "wraith" to Cargo.toml
- The [[bin]] entry for wraith-mcp-bridge confused Cargo about which
binary is the Tauri app main binary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The + button in the tab bar now shows a dropdown of detected local
shells. Clicking one opens a full-size PTY terminal in the main
content area as a proper tab — not the copilot sidebar.
- New "local" protocol type in Session interface
- LocalTerminalView component uses useTerminal(id, 'pty')
- SessionContainer renders local sessions alongside SSH/RDP
- TabBadge shows purple dot for local sessions
- Shell detection includes WSL (wsl.exe) on Windows
- closeSession handles PTY disconnect for local tabs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remote monitoring bar:
- Slim 24px bar at bottom of every SSH terminal
- CPU, RAM, disk, network stats polled every 5s via exec channel
- Cross-platform: Linux (/proc), macOS (vm_stat/sysctl), FreeBSD
- Color-coded thresholds: green/amber/red
- No agent installation — standard POSIX commands only
SFTP follows active tab:
- Added :key="activeSessionId" to FileTree component
- Vue recreates FileTree when session changes, reinitializing SFTP
CWD tracking fix (macOS + all platforms):
- Old approach: exec channel pwd — returns HOME, not actual CWD
- New approach: passive OSC 7 parsing in the output stream
- Scans for \e]7;file://host/path\a without modifying data
- Works with bash, zsh, fish on both Linux and macOS
- Zero corruption risk — data passes through unmodified
- Includes URL percent-decoding for paths with spaces
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resizable panel:
- Drag handle on left border, pointer events, 320px–1200px range
SFTP MCP tools:
- sftp_list, sftp_read, sftp_write — full HTTP endpoints + bridge tools
- SftpService now Clone for MCP server sharing
Active session context:
- mcp_get_session_context — last 20 lines of any session's scrollback
Error watcher:
- Background scanner every 2s across all sessions
- 20+ patterns: permission denied, OOM, segfault, disk full, etc.
- mcp:error events emitted to frontend
- Sessions auto-registered on SSH connect
Configurable launch presets:
- Settings → AI Copilot section with preset editor
- Name + command pairs, stored in settings table as JSON
- One-click preset buttons in copilot panel empty state
- Defaults: Claude Code, Gemini CLI, Codex CLI
- User can set custom commands (e.g. claude --dangerously-skip-permissions)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resizable panel:
- Drag handle on left border of copilot panel
- Pointer events for smooth resize (320px–1200px range)
SFTP MCP tools:
- sftp_list: list remote directories
- sftp_read: read remote files
- sftp_write: write remote files
- Full HTTP endpoints + bridge tool definitions
Active session context:
- mcp_get_session_context command returns last 20 lines of scrollback
- Frontend can call on tab switch to keep AI informed
Error watcher:
- Background scanner runs every 2 seconds across all sessions
- 20+ error patterns (permission denied, OOM, segfault, disk full, etc.)
- Emits mcp:error events to frontend with session ID and matched line
- Sessions auto-registered with watcher on connect
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Auto-inject CLAUDE_MCP_SERVERS env var when copilot PTY spawns,
so Claude Code auto-discovers wraith-mcp-bridge without manual config
- RDP screenshot_png_base64() encodes frame buffer as PNG via png crate
- Bridge binary exposes terminal_screenshot tool returning MCP image
content (base64 PNG with mimeType) for multimodal AI analysis
- MCP session list now includes RDP sessions with dimensions
- /mcp/screenshot HTTP endpoint on the internal server
"Screenshot that RDP session, what's the error?" now works.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete MCP communication pipeline:
Backend HTTP server (axum on localhost:0):
- POST /mcp/sessions — list active sessions
- POST /mcp/terminal/read — read scrollback (ANSI stripped)
- POST /mcp/terminal/execute — send command + marker, capture output
- Port written to data_dir/mcp-port at startup
- Shares SshService and ScrollbackRegistry with AppState via Clone
Bridge binary (wraith-mcp-bridge):
- Speaks JSON-RPC 2.0 over stdio (MCP protocol)
- Translates tool calls to HTTP requests against running Wraith
- Implements initialize, tools/list, tools/call
- Exposes: terminal_read, terminal_execute, list_sessions
- Reads MCP port from data_dir/mcp-port
Auto-config:
- PTY spawn injects WRAITH_MCP_BRIDGE env var
- SshService and ScrollbackRegistry derive Clone for sharing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Infrastructure for the Wraith Terminal MCP server:
- ScrollbackBuffer: 64KB circular buffer per session with ANSI stripping
- ScrollbackRegistry: DashMap registry shared between output loops and MCP
- SSH output loop feeds scrollback in addition to emitting events
- PTY output loop feeds scrollback in addition to emitting events
- mcp_terminal_read: read last N lines from any session (ANSI stripped)
- mcp_terminal_execute: send command + marker, capture output until marker
- mcp_list_sessions: enumerate all active SSH sessions with metadata
8 new scrollback tests (ring buffer, ANSI strip, line limiting).
95 total tests, zero warnings.
Bridge binary and auto-config injection to follow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tauri's built-in dragDropEnabled intercepts all drag events in the
WebView for native file drop handling. This prevents HTML5 drag-and-drop
between elements within the page. Disabled so tab reordering works.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Buttons resist drag in WebView2. Switched to div[role=tab] with
select-none. Added setData() call on dragstart — required by most
WebView implementations to actually initiate the drag operation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Status indicators:
- Session.status field tracks connected/disconnected state
- Listens for ssh:close and ssh:exit backend events
- Tab dot turns red when connection drops (green=SSH, blue=RDP, red=dead)
Draggable tabs:
- HTML5 drag-and-drop on tab buttons
- Blue left-border indicator shows drop target
- moveSession() in store reorders the sessions array
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tokio::task::spawn_blocking panics when called without a tokio runtime
context. Sync Tauri command handlers may not have one. The PTY reader
loop is long-lived anyway — std:🧵:spawn is more correct for
persistent blocking I/O and doesn't require a runtime guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ripped out the Gemini API stub (ai/mod.rs, ai_commands.rs, GeminiPanel.vue)
and replaced it with a local PTY terminal in the sidebar panel. Users select
a shell (bash/zsh/sh on Unix, PowerShell/CMD/Git Bash on Windows), launch it,
and run claude/gemini/codex or any CLI tool directly.
Backend:
- New PtyService module using portable-pty (cross-platform PTY)
- DashMap session registry (same pattern as SshService)
- spawn_blocking output loop (portable-pty reader is synchronous)
- 5 Tauri commands: list_available_shells, spawn_local_shell, pty_write,
pty_resize, disconnect_pty
Frontend:
- Parameterized useTerminal composable: backend='ssh'|'pty'
- convertEol=false for PTY (PTY driver handles LF→CRLF)
- CopilotPanel.vue with shell selector, launch/kill, session ended prompt
- Ctrl+Shift+G toggle preserved
Tests: 87 total (5 new PTY tests), zero warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced the openssl CLI fallback with pure Rust crypto for EC private
keys in SEC1 format (-----BEGIN EC PRIVATE KEY-----). Handles PKCS#5
encrypted keys (AES-128-CBC + MD5 EVP_BytesToKey KDF) and converts to
PKCS#8 PEM that russh can parse natively.
All crypto crates (md5, aes, cbc, sec1, pkcs8) were already in the dep
tree via russh — just promoted to direct dependencies. Zero new binary
dependencies, works on Windows without openssl installed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tauri's NSIS bundler creates a desktop shortcut unconditionally. Added
a POSTINSTALL hook that deletes it immediately after creation. Start
menu shortcut remains. Users who want a desktop shortcut can create
one manually.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a file picker next to the Private Key textarea in the credential
dialog. Users can browse for key files (.pem, .key, id_rsa, id_ed25519,
etc.) instead of copy-pasting PEM content. Uses standard FileReader API
— no additional Tauri plugins needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
russh only parses 4 PEM headers: OPENSSH, RSA, PKCS8, ENCRYPTED PKCS8.
EC keys (-----BEGIN EC PRIVATE KEY-----) with PKCS5 encryption silently
failed with "Could not read key".
Fix adds two fallbacks:
1. If russh can't parse the key, convert to PKCS8 via `openssl pkey`
which handles EC, DSA, and all other OpenSSL-supported formats
2. If the input doesn't start with -----BEGIN, try reading it as a
file path (supports ~ expansion) for keys stored on disk
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: The output reader loop held Arc<TokioMutex<Channel>> while
calling ch.wait().await. After the initial prompt rendered and the server
went idle, wait() blocked indefinitely holding the lock. ssh_write()
could never acquire the mutex to send keystrokes. Permanent deadlock.
Fix: Separated read/write paths. The output loop now owns the Channel
exclusively via tokio::select!, receiving resize/shutdown commands through
an mpsc channel. Writes go through Handle::data(channel_id, data) which
bypasses the Channel entirely — no shared mutex, no deadlock.
Also killed all compiler warnings (unused imports in rdp module).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Clipboard paste (rdp_send_clipboard): simulates typing each character
via scancode key press/release events. Full ASCII coverage including
all symbols, numbers, and shifted characters. Handles 32-char
generated passwords without manual typing.
2. Keyboard grab defaults to ON so RDP sessions accept keyboard input
immediately without requiring the user to click the toolbar toggle.
3. Frame dirty flag: GraphicsUpdate sets an AtomicBool, get_frame only
encodes + returns base64 when dirty (returns empty string otherwise).
Eliminates ~8MB/frame base64 encoding on unchanged frames at 30fps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>