Tool windows open/close immediately — diagnostic build:
- CSP set to null to eliminate it as a variable. The CSP was blocking
IPC initialization in child WebviewWindows on macOS WKWebView.
- Added tauri://error listeners to ALL WebviewWindow creations (tool,
help, editor, detach). If window creation fails on the Rust side,
an alert will show the error instead of silently closing.
- If tool windows work with csp:null, we know the CSP was the cause
and can craft a macOS-compatible policy. If they still fail, the
error alert will reveal the actual Rust-side error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cursor blinking:
- handleFocus() was referenced in TerminalView template but never
defined in script — clicking the terminal container threw a runtime
error preventing focus. Added the missing function.
- Removed duplicate handleFocus at end of file
Tool windows:
- CSP simplified for macOS WKWebView compatibility. Previous CSP
was blocking child WebviewWindow IPC initialization.
RDP tab switch:
- Added rdp_force_refresh command that marks frame_dirty=true and
clears dirty region, forcing next get_frame to return full buffer
- Called on tab activation and after resize completion
- Eliminates 4-5 second wait for "keyframe" when switching tabs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the Wraith window is resized, the RDP session now resizes to fill
the entire canvas area — matching MobaXTerm behavior.
Implementation:
- Enabled ironrdp displaycontrol feature for Display Control Virtual Channel
- Added Resize input event to RDP session thread
- ActiveStage::encode_resize() sends monitor layout PDU to server
- Server re-renders at new resolution and sends updated frames
- Frontend ResizeObserver on canvas wrapper, debounced 500ms
- Canvas CSS changed from max-width/max-height to width/height: 100%
- Mouse coordinate mapping uses canvas.width/height (actual resolution)
instead of initial rdpWidth/rdpHeight
- Image and front buffer reallocated on resize to match new dimensions
- Width constrained to even numbers per RDP spec, min 200px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: Every GraphicsUpdate copied the full 8.3MB decoded image
into the front buffer, cloned all 8.3MB for IPC, and transferred all
8.3MB to the frontend — even when only a 100x100 pixel region changed.
During window drag, this created a 25MB/frame pipeline backup.
Fix:
- Track dirty rectangles from ironrdp's GraphicsUpdate(InclusiveRectangle)
- Write path: only copy changed rows from decoded image to front buffer
(e.g. 100 rows × 1920 pixels = 768KB vs 8.3MB full frame)
- Accumulate dirty region as union of all rects since last get_frame
- Read path: if dirty region < 50% of frame, extract only the dirty
rectangle bytes; otherwise fall back to full frame
- Binary IPC format: 8-byte header [x,y,w,h as u16 LE] + pixel data
- Frontend: putImageData at dirty rect offset instead of full frame
- Status bar: h-9 text-sm for 3440x1440 readability
During window drag (typical 300x400 dirty rect):
Before: 8.3MB write + 8.3MB clone + 8.3MB IPC = 24.9MB per frame
After: 480KB write + 480KB extract + 480KB IPC = 1.4MB per frame
~17x reduction in data movement per frame.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Popup windows (tools/editor/help):
- CSP broadened for macOS: added tauri: and https://tauri.localhost to
default-src, https://ipc.localhost and tauri: to connect-src. WKWebView
uses different IPC scheme than Windows WebView2.
Theme application:
- terminal.refresh() after theme change forces xterm.js canvas repaint
of existing text — was only changing background, not text colors
Selection highlighting:
- Removed CSS .xterm-selection div rule entirely — xterm.js v6 canvas
renderer doesn't use DOM selection divs, the CSS was a no-op that
conflicted with theme-driven selectionBackground
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Popup windows (tools/editor/help):
- CSP script-src 'self' blocked Tauri's inline IPC bridge scripts in
child WebviewWindows. Added 'unsafe-inline' to script-src. Still
restrictive (was null before SEC-4).
Theme application:
- Watcher on sessionStore.activeTheme needed { deep: true } — Pinia
reactive proxy identity doesn't change on object replacement
- LocalTerminalView.vue had ZERO theme support — added full applyTheme()
with watcher and mount-time application
- Container background now syncs with theme (was stuck on CSS variable)
Cursor blink:
- terminal.focus() after mount in useTerminal.ts — terminal opened
without focus, xterm.js rendered static outline instead of blinking block
Selection highlighting:
- applyTheme() was overwriting theme without selectionBackground/
selectionForeground/selectionInactiveBackground — selection invisible
after any theme change
- Removed !important from terminal.css that overrode canvas selection
- Bumped default selection opacity 0.3 → 0.4
Status bar:
- h-6 text-[10px] → h-8 text-xs (24px/10px → 32px/12px)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
additionalBrowserArgs: --enable-gpu-rasterization --enable-zero-copy
Also re-disables msWebOOUI/msPdfOOUI/msSmartScreenProtection (wry defaults
that must be explicitly set when using additionalBrowserArgs).
macOS/Linux: no-op (WKWebView/WebKitGTK manage GPU natively).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: RDP was unresponsive due to frame pipeline bottleneck.
- get_frame() held tokio::Mutex while cloning 8.3MB, blocking the
RDP session thread from writing new frames (mutex contention)
- Frontend fetched on every backend event with no coalescing
- Every GraphicsUpdate emitted an IPC event, flooding the frontend
Fix:
- Double-buffer: back_buffer (tokio::Mutex, write path) and
front_buffer (std::sync::RwLock, read path) — reads never block writes
- get_frame() now synchronous, reads from front_buffer via RwLock
- Backend throttles frame events to every other GraphicsUpdate
- Frontend coalesces events via requestAnimationFrame
- RdpView props now reactive (computed) for correct resize behavior
- rdp_get_frame command no longer async (no .await needed)
- screenshot_png_base64 no longer async
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- percent_decode: collect bytes into Vec<u8> then String::from_utf8_lossy to handle
non-ASCII paths correctly instead of casting byte as char (corrupted codepoints > 127)
- format_mtime: remove dead `st`/`y`/`_y` variables and unused UNIX_EPOCH/Duration imports
- reorder_connections/reorder_groups: wrap UPDATE loops in BEGIN/COMMIT transactions
with ROLLBACK on error to prevent partial sort order writes
- scan_network: validate subnet matches 3-octet format before use in remote shell commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New shell_escape() utility for safe command interpolation
- Applied across all MCP tools, docker, scanner, network commands
- MCP server generates random bearer token at startup
- Token written to mcp-token file with 0600 permissions
- All MCP HTTP requests require Authorization header
- Bridge binary reads token and sends on every request
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Vault key uses Zeroizing<[u8; 32]>, passwords zeroized after use
- vault/credentials Mutex upgraded to tokio::sync::Mutex
- CWD tracker + monitor use CancellationToken for clean shutdown
- Monitor exec_command has 10s timeout, 3-strike dead connection heuristic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was only setting the remote clipboard without pasting. Now sends
clipboard content then simulates Ctrl+V (scancode 0x001D + 0x002F)
with 50ms delay for clipboard propagation. Works for any text
including special characters and multi-line content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The AI can now open its own SSH sessions without the Commander
pre-opening them:
- ssh_connect(hostname, username, password?, private_key_path?, port?)
Returns session_id for use with all other tools
Supports password auth and SSH key file auth
Also added app_handle and error_watcher to MCP server state so
new sessions get full scrollback, monitoring, and CWD tracking.
This completes the autonomy loop: the AI discovers what's available
(list_sessions), connects to what it needs (ssh_connect), operates
(terminal_execute, docker_ps, sftp_read), and disconnects when done.
Total MCP tools: 31.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 new MCP tools completing the RDP interaction loop:
- rdp_click — click at x,y coordinates (left/right/middle button)
Use terminal_screenshot first to identify coordinates
- rdp_type — type text into RDP session via clipboard paste
- rdp_clipboard — set clipboard content on remote desktop
The AI can now screenshot an RDP session, analyze it visually,
click buttons, type text, and read clipboard content. Full GUI
automation through the MCP bridge.
Total MCP tools: 30.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8 new MCP tools exposed through the bridge:
Docker:
- docker_ps — list all containers with status/image/ports
- docker_action — start/stop/restart/remove/logs/builder-prune/system-prune
- docker_exec — execute command inside a running container
System:
- service_status — check systemd service status
- process_list — ps aux with optional name filter
Git (remote repos):
- git_status — branch, dirty files, ahead/behind
- git_pull — pull latest changes
- git_log — recent 20 commits
Total MCP tools: 27. All accessible through the wraith-mcp-bridge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RDP performance overhaul:
- Switched from polling (rAF loop calling rdp_get_frame every tick)
to event-driven rendering (backend emits rdp:frame:{id} when
frame buffer updates, frontend fetches on demand)
- Eliminates thousands of empty IPC round-trips per second when
the screen is idle
- Backend passes AppHandle into run_active_session for event emission
- Frontend uses listen() instead of requestAnimationFrame polling
MCP terminal fix:
- terminal_type and terminal_execute now send \r (carriage return)
instead of \n (newline) — PTY terminals expect CR to submit
- Fixes commands not auto-sending, requiring manual Enter press
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
terminal_type MCP tool (19 tools total):
- Fire-and-forget text input to any session
- Optional press_enter flag (default: true)
- No marker wrapping, no output capture
- Use case: AI sends messages/commands without needing output back
Tab resize fix:
- Double requestAnimationFrame before fitAddon.fit() on tab switch
- Container has real dimensions after browser layout pass
- Also sends ssh_resize/pty_resize to backend with correct cols/rows
- Fixes 6-8 char wide terminals after switching tabs
Close confirmation:
- beforeunload event shows browser "Leave page?" dialog
- Only triggers if sessions are active
- Synchronous — cannot hang the close like onCloseRequested did
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DashMap::clone() deep-copies all entries into a new map. The MCP
server's cloned SshService/SftpService/RdpService/ScrollbackRegistry
were snapshots from startup that never saw new sessions.
Fix: wrap all DashMap fields in Arc<DashMap<...>> so clones share
the same underlying map. Sessions added after MCP startup are now
visible to MCP tools.
Affected: SshService, SftpService, RdpService, ScrollbackRegistry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Help menu (File → Help):
- Getting Started guide (connections, SFTP, copilot, tabs)
- Keyboard Shortcuts reference table
- MCP Integration page (setup command, all 18 tools documented,
bridge path auto-populated, architecture explanation)
- About page with version and tech stack
- Opens as a tabbed popup window
Tab detach fixes:
- Added detached-*, editor-*, help-* to capabilities window list
(detached windows had no event permissions — silent failure)
- SessionContainer filters out detached sessions (active=false)
so the main window stops rendering the terminal when detached
- Terminal now only renders in the detached popup window
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On startup, Wraith checks if wraith-mcp-bridge exists in the data
directory. If missing or version mismatch, downloads the correct
version from Gitea packages automatically. No installer changes needed.
Flow:
1. Check data_dir/wraith-mcp-bridge.exe exists
2. Check data_dir/mcp-bridge-version matches app version
3. If not, download from packages/vstockwell/generic/wraith/{ver}/
4. Set execute permissions on Unix
5. Write version marker
Also exposes mcp_bridge_path command so the frontend can show the
path in settings for users to add to PATH or configure Claude Code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Removed useless CLAUDE_MCP_SERVERS env var injection (doesn't work)
- CI builds wraith-mcp-bridge.exe as a separate cargo --bin step
- Bridge binary signed with EV cert alongside the installer
- Uploaded to Gitea packages per version
- Attached to Gitea release as a downloadable asset
- Users add to PATH then: claude mcp add wraith -- wraith-mcp-bridge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tauri auto-updater:
- Signing pubkey in tauri.conf.json
- tauri-plugin-updater initialized in lib.rs
- CI workflow passes TAURI_SIGNING_PRIVATE_KEY env vars to cargo tauri build
- CI generates update.json manifest with signature and uploads to
packages/latest/update.json endpoint
- Frontend checks for updates on startup via @tauri-apps/plugin-updater
- Downloads, installs, and relaunches seamlessly
- Settings → About button uses native updater too
RDP vault credentials:
- RDP connections now resolve credentials from vault via credentialId
- Same path as SSH: list_credentials → find by ID → decrypt_password
- Falls back to conn.options JSON if no vault credential linked
- Fixes blank username in RDP connect
Sidebar drag persist:
- reorder_connections and reorder_groups Tauri commands
- Batch-update sort_order in database on drop
- Order survives app restart
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same root cause as the PTY crash (v1.2.6): tokio::spawn called from
Tauri setup hook without a tokio runtime guard. Switched error watcher
to std:🧵:spawn. Also wrapped both error watcher and MCP server
spawn in individual catch_unwind blocks so neither can crash the app.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tab activity notifications:
- Background tabs pulse blue when new output arrives
- Clears when you switch to the tab
- useTerminal marks activity on every data event
Session persistence:
- Workspace saved to DB on window close (connection IDs + positions)
- Restored on launch — auto-reconnects saved sessions in order
- workspace_commands: save_workspace, load_workspace
Docker Manager (Tools → Docker Manager):
- Containers tab: list all, start/stop/restart/remove/logs
- Images tab: list all, remove
- Volumes tab: list all, remove
- One-click Builder Prune and System Prune buttons
- All operations via SSH exec channels — no Docker socket exposure
Sidebar drag-and-drop:
- Drag groups to reorder
- Drag connections between groups
- Drag connections within a group to reorder
- Blue border indicator on drop targets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MCP: RdpService had a manual Clone impl that called unreachable!().
Replaced with a real clone that shares the DashMap. MCP server can
now clone all services and start successfully.
RDP: rustls needs CryptoProvider::install_default() before any TLS
operations. ironrdp-tls uses rustls for the RDP TLS handshake.
Added aws_lc_rs provider installation at app startup.
Both panics found via wraith.log debug logging from v1.6.3.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>