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>
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: 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>
Rust RDP service: ironrdp client with full connection handshake
(TCP -> TLS -> CredSSP -> NLA), pixel buffer frame delivery,
mouse/keyboard input via scancode mapping, graceful disconnect.
Runs in dedicated thread with own tokio runtime to avoid Send
lifetime issues with ironrdp trait objects.
Vue frontend: RdpView canvas renderer with 30fps polling,
mouse/keyboard capture, RdpToolbar with Ctrl+Alt+Del and
clipboard. SessionContainer handles both SSH and RDP tabs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>