From 2838af4ee794da36aa73ef45726156cdad34c4ae Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Mon, 30 Mar 2026 10:58:45 -0400 Subject: [PATCH] =?UTF-8?q?fix:=206=20UX=20regressions=20=E2=80=94=20popup?= =?UTF-8?q?s,=20themes,=20cursor,=20selection,=20status=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src-tauri/src/commands/rdp_commands.rs | 10 ++++++++++ src-tauri/src/lib.rs | 2 +- src-tauri/src/rdp/mod.rs | 8 ++++++++ src-tauri/tauri.conf.json | 2 +- src/components/rdp/RdpView.vue | 17 +++++++++++------ src/components/terminal/TerminalView.vue | 8 ++++---- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/commands/rdp_commands.rs b/src-tauri/src/commands/rdp_commands.rs index 0a9858c..5a609a8 100644 --- a/src-tauri/src/commands/rdp_commands.rs +++ b/src-tauri/src/commands/rdp_commands.rs @@ -101,6 +101,16 @@ pub fn rdp_send_clipboard( 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. /// Sends a Display Control Virtual Channel request to the server. /// The server will re-render at the new resolution and send updated frames. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b68cb2e..de2f234 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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::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::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::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, diff --git a/src-tauri/src/rdp/mod.rs b/src-tauri/src/rdp/mod.rs index 93affda..a6757fb 100644 --- a/src-tauri/src/rdp/mod.rs +++ b/src-tauri/src/rdp/mod.rs @@ -300,6 +300,14 @@ impl RdpService { 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> { 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)) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f6670e1..c167224 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -23,7 +23,7 @@ } ], "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 }, diff --git a/src/components/rdp/RdpView.vue b/src/components/rdp/RdpView.vue index 75d87e6..f1cdea0 100644 --- a/src/components/rdp/RdpView.vue +++ b/src/components/rdp/RdpView.vue @@ -181,11 +181,14 @@ onMounted(() => { width: newW, height: newH, }).then(() => { - // Update canvas dimensions to match new RDP resolution if (canvasRef.value) { canvasRef.value.width = newW; 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) => { console.warn("[RdpView] resize failed:", err); }); @@ -201,14 +204,16 @@ onBeforeUnmount(() => { 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( () => props.isActive, (active) => { - if (active && keyboardGrabbed.value && canvasRef.value) { - setTimeout(() => { - canvasRef.value?.focus(); - }, 0); + if (active && canvasRef.value) { + // Force full frame fetch to repaint the canvas immediately + invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {}); + if (keyboardGrabbed.value) { + setTimeout(() => canvasRef.value?.focus(), 0); + } } }, ); diff --git a/src/components/terminal/TerminalView.vue b/src/components/terminal/TerminalView.vue index 50377c7..cfe5e85 100644 --- a/src/components/terminal/TerminalView.vue +++ b/src/components/terminal/TerminalView.vue @@ -152,6 +152,10 @@ onMounted(() => { }, 50); }); +function handleFocus(): void { + terminal.focus(); +} + // Re-fit and focus terminal when switching back to this tab. // Must wait for the container to have real dimensions after becoming visible. watch( @@ -229,8 +233,4 @@ onBeforeUnmount(() => { resizeDisposable = null; } }); - -function handleFocus(): void { - terminal.focus(); -}