Tool/help/editor/detach windows:
- Moved ALL child window creation from JS-side WebviewWindow to
Rust-side WebviewWindowBuilder via new open_child_window command.
JS WebviewWindow on macOS WKWebView was creating windows that
never fully initialized — the webview content process failed
silently. Rust-side creation uses the proper main thread context.
- All four call sites (tool, help, editor, detach) now use invoke()
- Errors surface as alert() instead of silent failure
RDP tab switch:
- Immediate force_refresh on tab activation for instant visual feedback
- 300ms delayed dimension check (was double-rAF which was too fast)
- If dimensions changed, resize + 500ms delayed refresh for clean repaint
- Fixes 3/4 resolution rendering after copilot panel toggle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per Gemini's analysis: WKWebView may reap windows that fail to
establish focus during creation. All WebviewWindow instances now
created with visible: false + focus: true, then wv.show() is
called only after tauri://created confirms the webview is ready.
This prevents the OS window manager from treating the window as
an orphaned popup during the brief initialization period.
Applied to: tool windows, help windows, editor windows, detached
session windows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tool windows (still closing instantly after every prior fix):
- Changed ToolWindow from defineAsyncComponent to direct synchronous
import. All 14 tool components now bundled into the main JS chunk.
Eliminates async chunk loading as a failure point — if the main
bundle loads (which it does, since the main window works), the
tool window code is guaranteed to be available.
- ToolWindow chunk no longer exists as a separate file
Status bar + Monitor bar:
- Both set to h-[48px] text-base px-6 (48px height, 16px text)
- Matching sizes for visual consistency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
.terminal-container had both height: 100% (CSS) and flex-1 (Tailwind).
In the flex-col parent, height: 100% forced the terminal to claim the
full parent height, squeezing MonitorBar below its h-6 minimum.
Fix: replaced height: 100% with min-height: 0. flex-1 handles sizing,
min-height: 0 allows proper flex shrinking so MonitorBar gets its full
24px allocation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both bars now identical: 24px height, 10px font. MonitorBar had been
changed during debugging — reverted to match StatusBar exactly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When switching from SSH back to RDP, the canvas retained the resolution
from when the copilot panel was open — even after closing the panel.
The ResizeObserver doesn't fire while the tab is hidden (v-show/display),
so the container size change goes unnoticed.
Fix: On tab activation, double-rAF waits for layout, measures the
container via getBoundingClientRect, compares with canvas.width/height,
and sends rdp_resize if they differ. This ensures the RDP session
always matches the current available space.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tool windows (still closing instantly with CSP=null):
- Root cause: Vite adds crossorigin attribute to <script> and <link> tags
in index.html. This forces CORS mode for resource loading. WKWebView's
Tauri custom protocol handler (tauri://) may not return proper CORS
headers for child WebviewWindows, causing the module script to fail
to load and the window to close immediately.
- Fix: Vite plugin strips crossorigin from built HTML via transformIndexHtml
- Also set crossOriginLoading: false for Rollup output chunks
Status bar:
- h-[48px] text-base px-6 — 48px height with 16px text, explicit pixel
value to avoid Tailwind spacing ambiguity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 blink + selection (ROOT CAUSE FOUND):
- xterm.js v6 uses DOM renderer, not canvas. Cursor blink and selection
both require CSS focus classes set by the hidden textarea's focus event.
- WKWebView doesn't focus elements with opacity:0, width:0, height:0,
left:-9999em — the textarea never receives focus, classes never toggle.
- Fix: Override .xterm-helper-textarea to left:0, top:0, width:1px,
height:1px, opacity:0.01 — within viewport, non-zero, focusable.
Theme restoration on startup:
- sessionStore.activeTheme started as null on every launch
- ThemePicker saved active_theme to settings but nobody restored it
- Added theme restoration to MainLayout onMounted — reads active_theme
setting, fetches theme from backend, calls setTheme() before any
terminals open
Status bar:
- h-10 (40px) to match toolbar height for visual balance
Selection colors:
- Solid #264f78 (VS Code selection blue) instead of rgba transparency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cursor blinking (ACTUALLY fixed this time):
- handleFocus() was STILL missing due to botched duplicate removal in
v1.12.1. Now defined once at line 80, no duplicates.
- vue-tsc --noEmit now runs clean (was erroring with TS2393)
Tool windows:
- App.vue now has onErrorCaptured + error display overlay so tool window
crashes show the error instead of silently closing
- defineAsyncComponent uses object form with onError callback for logging
Selection highlighting:
- Changed from rgba(88,166,255,0.4) transparent to solid #264f78
(VS Code's selection color) — always visible on dark backgrounds
- Applied to default theme, TerminalView applyTheme, LocalTerminalView
CSP:
- Simplified to 'self' 'unsafe-inline' asset: for default-src
- Separate connect-src for IPC protocols
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>
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>
- Convert all ~40 inline styles in UnlockLayout.vue to Tailwind CSS v4 arbitrary-value classes, matching MainLayout.vue color conventions (CSS variables + hex arbitraries). Visual appearance preserved exactly.
- Create ToolShell.vue reusable wrapper that owns output/running state and execute/setOutput API via defineExpose.
- Refactor PingTool, TracerouteTool, DnsLookup, WhoisTool, BandwidthTest to use ToolShell — each tool now contains only its unique inputs and invoke calls. Zero vue-tsc errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract handleKeydown into useKeyboardShortcuts.ts composable; reduces
MainLayout by ~20 lines and isolates keyboard logic cleanly
- ConnectionTree: watch groups for additions and auto-expand new entries
- MonitorBar: generation counter prevents stale event listeners on rapid
tab switching
- NetworkScanner: revoke blob URL after CSV export click (memory leak)
- TransferProgress: implement the auto-expand/collapse watcher that was
only commented but never wired up
- FileTree: block binary/large file uploads with clear user error rather
than silently corrupting — backend sftp_write_file is text-only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TabBar: move listen() into onMounted, store UnlistenFn
- Session store: track per-session unlisten fns, clean on close
- useRdp: store frame unlisten in composable scope, call in stopFrameLoop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VUE-1: store workspace save interval ID, clear in onUnmounted
VUE-2: extract beforeunload handler to named function, remove in onUnmounted
VUE-3: move useTerminal() to <script setup> top level in DetachedSession
VUE-4: call useTerminal() before nextTick await in CopilotPanel launch()
VUE-5: remove duplicate ResizeObserver from LocalTerminalView (useTerminal already creates one)
VUE-6: store terminal.onResize() IDisposable, dispose in onBeforeUnmount
VUE-7: extract connectSsh(), connectRdp(), resolveCredentials() from 220-line connect()
VUE-8: check session protocol before ssh_resize vs pty_resize in TerminalView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resize fix:
- ResizeObserver now checks contentRect dimensions before fitting
- Ignores resize events when container width/height < 50px (hidden tab)
- Prevents xterm.js from calculating 8-char columns on zero-width containers
Flickering fix:
- markActivity throttled to once per second per session
- Was firing on every single data event (hundreds per second during
active output), triggering Vue reactivity updates on the sessions
array, causing tab bar and session container re-renders
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>
onCloseRequested async handler was permanently blocking window close
on Windows, even with the 2s timeout. The confirm() dialog and async
invoke chain prevented the close event from completing.
Fix: removed onCloseRequested entirely. Workspace now auto-saves
every 30 seconds via setInterval. Close button works immediately
with no handler blocking it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Tauri native updater needs update.json hosted at a static URL.
Gitea packages don't support a 'latest' alias, so the endpoint
returned 'package does not exist'. Reverted Settings and startup
check to use check_for_updates command which queries the Gitea
releases API directly and works reliably.
The native auto-updater will work once we have proper static hosting
for update.json (or a redirect endpoint). For now, the manual check
+ download page approach is functional.
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>
The onCloseRequested async handler was blocking window close when
save_workspace invoke hung or threw. Fixed with:
1. Confirmation dialog: "Are you sure you want to close Wraith?"
(only shown if sessions are active, cancel prevents close)
2. Workspace save wrapped in Promise.race with 2s timeout so a
stuck invoke can never block the close indefinitely
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues:
1. convertEol was false for PTY sessions, but Windows ConPTY sends
bare \n without \r. Now enabled on Windows PTY sessions (checked
via navigator.platform). Unix PTY still false (driver handles it).
2. LocalTerminalView had no ResizeObserver, so the terminal never
reflowed when the container size changed. Added ResizeObserver
matching the SSH TerminalView pattern. Also added proper cleanup
on unmount.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Right-click any tab → "Detach to Window" opens the session in its
own Tauri window. The tab dims (opacity + italic) while detached.
Close the detached window → session reattaches to the main tab bar.
Architecture:
- DetachedSession.vue: standalone terminal that connects to the same
backend session (SSH/PTY events keep flowing)
- App.vue detects #/detached-session?sessionId=X hash
- Tab context menu: Detach to Window, Close
- session:reattach event emitted on window close, main window listens
- Monitor bar included in detached SSH windows
- Session.active flag tracks detached state
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>
Workspace restore was running synchronously in onMounted which could
crash if saved connection IDs were stale. The import of
@tauri-apps/api/window for onCloseRequested could also fail in
certain contexts.
Fix: defer restore to setTimeout(500ms) so the app renders first,
wrap each reconnect in individual try/catch, wrap the window close
listener setup in try/catch.
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>
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>
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>
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>
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>
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>
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>