fix: 6 UX regressions — popups, themes, cursor, selection, status bar
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m54s

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>
This commit is contained in:
Vantz Stockwell 2026-03-30 10:58:45 -04:00
parent 09c2f1a1ff
commit 2838af4ee7
6 changed files with 35 additions and 12 deletions

View File

@ -101,6 +101,16 @@ pub fn rdp_send_clipboard(
state.rdp.send_clipboard(&session_id, &text) state.rdp.send_clipboard(&session_id, &text)
} }
/// Force the next get_frame to return a full frame regardless of dirty state.
/// Used when switching tabs or after resize to ensure the canvas is fully repainted.
#[tauri::command]
pub fn rdp_force_refresh(
session_id: String,
state: State<'_, AppState>,
) -> Result<(), String> {
state.rdp.force_refresh(&session_id)
}
/// Resize the RDP session's desktop resolution. /// Resize the RDP session's desktop resolution.
/// Sends a Display Control Virtual Channel request to the server. /// Sends a Display Control Virtual Channel request to the server.
/// The server will re-render at the new resolution and send updated frames. /// The server will re-render at the new resolution and send updated frames.

View File

@ -224,7 +224,7 @@ pub fn run() {
commands::credentials::list_credentials, commands::credentials::create_password, commands::credentials::create_ssh_key, commands::credentials::delete_credential, commands::credentials::decrypt_password, commands::credentials::decrypt_ssh_key, commands::credentials::list_credentials, commands::credentials::create_password, commands::credentials::create_ssh_key, commands::credentials::delete_credential, commands::credentials::decrypt_password, commands::credentials::decrypt_ssh_key,
commands::ssh_commands::connect_ssh, commands::ssh_commands::connect_ssh_with_key, commands::ssh_commands::ssh_write, commands::ssh_commands::ssh_resize, commands::ssh_commands::disconnect_ssh, commands::ssh_commands::disconnect_session, commands::ssh_commands::list_ssh_sessions, commands::ssh_commands::connect_ssh, commands::ssh_commands::connect_ssh_with_key, commands::ssh_commands::ssh_write, commands::ssh_commands::ssh_resize, commands::ssh_commands::disconnect_ssh, commands::ssh_commands::disconnect_session, commands::ssh_commands::list_ssh_sessions,
commands::sftp_commands::sftp_list, commands::sftp_commands::sftp_read_file, commands::sftp_commands::sftp_write_file, commands::sftp_commands::sftp_mkdir, commands::sftp_commands::sftp_delete, commands::sftp_commands::sftp_rename, commands::sftp_commands::sftp_list, commands::sftp_commands::sftp_read_file, commands::sftp_commands::sftp_write_file, commands::sftp_commands::sftp_mkdir, commands::sftp_commands::sftp_delete, commands::sftp_commands::sftp_rename,
commands::rdp_commands::connect_rdp, commands::rdp_commands::rdp_get_frame, commands::rdp_commands::rdp_send_mouse, commands::rdp_commands::rdp_send_key, commands::rdp_commands::rdp_send_clipboard, commands::rdp_commands::rdp_resize, commands::rdp_commands::disconnect_rdp, commands::rdp_commands::list_rdp_sessions, commands::rdp_commands::connect_rdp, commands::rdp_commands::rdp_get_frame, commands::rdp_commands::rdp_force_refresh, commands::rdp_commands::rdp_send_mouse, commands::rdp_commands::rdp_send_key, commands::rdp_commands::rdp_send_clipboard, commands::rdp_commands::rdp_resize, commands::rdp_commands::disconnect_rdp, commands::rdp_commands::list_rdp_sessions,
commands::theme_commands::list_themes, commands::theme_commands::get_theme, commands::theme_commands::list_themes, commands::theme_commands::get_theme,
commands::pty_commands::list_available_shells, commands::pty_commands::spawn_local_shell, commands::pty_commands::pty_write, commands::pty_commands::pty_resize, commands::pty_commands::disconnect_pty, commands::pty_commands::list_available_shells, commands::pty_commands::spawn_local_shell, commands::pty_commands::pty_write, commands::pty_commands::pty_resize, commands::pty_commands::disconnect_pty,
commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context, commands::mcp_commands::mcp_bridge_path, commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context, commands::mcp_commands::mcp_bridge_path,

View File

@ -300,6 +300,14 @@ impl RdpService {
handle.input_tx.send(InputEvent::Key { scancode, pressed }).map_err(|_| format!("RDP session {} input channel closed", session_id)) handle.input_tx.send(InputEvent::Key { scancode, pressed }).map_err(|_| format!("RDP session {} input channel closed", session_id))
} }
pub fn force_refresh(&self, session_id: &str) -> Result<(), String> {
let handle = self.sessions.get(session_id).ok_or_else(|| format!("RDP session {} not found", session_id))?;
// Clear any accumulated dirty region so get_frame returns the full buffer
*handle.dirty_region.lock().unwrap_or_else(|e| e.into_inner()) = None;
handle.frame_dirty.store(true, Ordering::Release);
Ok(())
}
pub fn resize(&self, session_id: &str, width: u16, height: u16) -> Result<(), String> { pub fn resize(&self, session_id: &str, width: u16, height: u16) -> Result<(), String> {
let handle = self.sessions.get(session_id).ok_or_else(|| format!("RDP session {} not found", session_id))?; let handle = self.sessions.get(session_id).ok_or_else(|| format!("RDP session {} not found", session_id))?;
handle.input_tx.send(InputEvent::Resize { width, height }).map_err(|_| format!("RDP session {} input channel closed", session_id)) handle.input_tx.send(InputEvent::Resize { width, height }).map_err(|_| format!("RDP session {} input channel closed", session_id))

View File

@ -23,7 +23,7 @@
} }
], ],
"security": { "security": {
"csp": "default-src 'self' tauri: https://tauri.localhost; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost data:; connect-src 'self' ipc: http://ipc.localhost https://ipc.localhost tauri:" "csp": "default-src 'self' 'unsafe-inline' asset: https://asset.localhost; img-src 'self' asset: https://asset.localhost data: blob:; connect-src ipc: http://ipc.localhost https://ipc.localhost"
}, },
"withGlobalTauri": false "withGlobalTauri": false
}, },

View File

@ -181,11 +181,14 @@ onMounted(() => {
width: newW, width: newW,
height: newH, height: newH,
}).then(() => { }).then(() => {
// Update canvas dimensions to match new RDP resolution
if (canvasRef.value) { if (canvasRef.value) {
canvasRef.value.width = newW; canvasRef.value.width = newW;
canvasRef.value.height = newH; canvasRef.value.height = newH;
} }
// Force full frame after resize so canvas gets a clean repaint
setTimeout(() => {
invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {});
}, 200);
}).catch((err: unknown) => { }).catch((err: unknown) => {
console.warn("[RdpView] resize failed:", err); console.warn("[RdpView] resize failed:", err);
}); });
@ -201,14 +204,16 @@ onBeforeUnmount(() => {
if (resizeTimeout) { clearTimeout(resizeTimeout); resizeTimeout = null; } if (resizeTimeout) { clearTimeout(resizeTimeout); resizeTimeout = null; }
}); });
// Focus canvas when this tab becomes active and keyboard is grabbed // Focus canvas and force full frame refresh when switching to this tab
watch( watch(
() => props.isActive, () => props.isActive,
(active) => { (active) => {
if (active && keyboardGrabbed.value && canvasRef.value) { if (active && canvasRef.value) {
setTimeout(() => { // Force full frame fetch to repaint the canvas immediately
canvasRef.value?.focus(); invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {});
}, 0); if (keyboardGrabbed.value) {
setTimeout(() => canvasRef.value?.focus(), 0);
}
} }
}, },
); );

View File

@ -152,6 +152,10 @@ onMounted(() => {
}, 50); }, 50);
}); });
function handleFocus(): void {
terminal.focus();
}
// Re-fit and focus terminal when switching back to this tab. // Re-fit and focus terminal when switching back to this tab.
// Must wait for the container to have real dimensions after becoming visible. // Must wait for the container to have real dimensions after becoming visible.
watch( watch(
@ -229,8 +233,4 @@ onBeforeUnmount(() => {
resizeDisposable = null; resizeDisposable = null;
} }
}); });
function handleFocus(): void {
terminal.focus();
}
</script> </script>