fix: Rust-side window creation + RDP tab switch layout delay
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m51s

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>
This commit is contained in:
Vantz Stockwell 2026-03-30 14:29:14 -04:00
parent d462381cce
commit 703ebdd557
6 changed files with 83 additions and 85 deletions

View File

@ -14,3 +14,4 @@ pub mod updater;
pub mod tools_commands_r2; pub mod tools_commands_r2;
pub mod workspace_commands; pub mod workspace_commands;
pub mod docker_commands; pub mod docker_commands;
pub mod window_commands;

View File

@ -0,0 +1,24 @@
use tauri::AppHandle;
use tauri::WebviewWindowBuilder;
/// Open a child window from the Rust side using WebviewWindowBuilder.
/// This is more reliable than JS-side WebviewWindow on macOS WKWebView.
#[tauri::command]
pub async fn open_child_window(
app_handle: AppHandle,
label: String,
title: String,
url: String,
width: f64,
height: f64,
) -> Result<(), String> {
let webview_url = tauri::WebviewUrl::App(url.into());
WebviewWindowBuilder::new(&app_handle, &label, webview_url)
.title(&title)
.inner_size(width, height)
.resizable(true)
.center()
.build()
.map_err(|e| format!("Failed to create window '{}': {}", label, e))?;
Ok(())
}

View File

@ -234,6 +234,7 @@ pub fn run() {
commands::updater::check_for_updates, commands::updater::check_for_updates,
commands::workspace_commands::save_workspace, commands::workspace_commands::load_workspace, commands::workspace_commands::save_workspace, commands::workspace_commands::load_workspace,
commands::docker_commands::docker_list_containers, commands::docker_commands::docker_list_images, commands::docker_commands::docker_list_volumes, commands::docker_commands::docker_action, commands::docker_commands::docker_list_containers, commands::docker_commands::docker_list_images, commands::docker_commands::docker_list_volumes, commands::docker_commands::docker_action,
commands::window_commands::open_child_window,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@ -204,44 +204,45 @@ onBeforeUnmount(() => {
if (resizeTimeout) { clearTimeout(resizeTimeout); resizeTimeout = null; } if (resizeTimeout) { clearTimeout(resizeTimeout); resizeTimeout = null; }
}); });
// Focus canvas, re-check dimensions, and force full frame on tab switch // Focus canvas, re-check dimensions, and force full frame on tab switch.
// Uses 300ms delay to let the flex layout fully settle (copilot panel toggle, etc.)
watch( watch(
() => props.isActive, () => props.isActive,
(active) => { (active) => {
if (!active || !canvasRef.value) return; if (!active || !canvasRef.value) return;
// Wait for layout to settle after tab becomes visible // Immediate focus so keyboard works right away
requestAnimationFrame(() => { if (keyboardGrabbed.value) canvasRef.value.focus();
requestAnimationFrame(() => {
const wrapper = canvasWrapper.value;
const canvas = canvasRef.value;
if (!wrapper || !canvas) return;
const { width: cw, height: ch } = wrapper.getBoundingClientRect(); // Immediate force refresh to show SOMETHING while we check dimensions
const newW = Math.round(cw) & ~1; invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {});
const newH = Math.round(ch);
// If container size differs from canvas resolution, resize the RDP session // Delayed dimension check layout needs time to settle
if (newW >= 200 && newH >= 200 && (newW !== canvas.width || newH !== canvas.height)) { setTimeout(() => {
invoke("rdp_resize", { const wrapper = canvasWrapper.value;
sessionId: props.sessionId, const canvas = canvasRef.value;
width: newW, if (!wrapper || !canvas) return;
height: newH,
}).then(() => { const { width: cw, height: ch } = wrapper.getBoundingClientRect();
const newW = Math.round(cw) & ~1;
const newH = Math.round(ch);
if (newW >= 200 && newH >= 200 && (newW !== canvas.width || newH !== canvas.height)) {
invoke("rdp_resize", {
sessionId: props.sessionId,
width: newW,
height: newH,
}).then(() => {
if (canvas) {
canvas.width = newW; canvas.width = newW;
canvas.height = newH; canvas.height = newH;
setTimeout(() => { }
invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {}); setTimeout(() => {
}, 200); invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {});
}).catch(() => {}); }, 500);
} else { }).catch(() => {});
// Same size just refresh the frame }
invoke("rdp_force_refresh", { sessionId: props.sessionId }).catch(() => {}); }, 300);
}
if (keyboardGrabbed.value) canvas.focus();
});
});
}, },
); );
</script> </script>

View File

@ -133,20 +133,14 @@ async function detachTab(): Promise<void> {
session.active = false; session.active = false;
// Open a new Tauri window for this session // Open a new Tauri window for this session
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); try {
const label = `detached-${session.id.substring(0, 8)}-${Date.now()}`; await invoke("open_child_window", {
const wv = new WebviewWindow(label, { label: `detached-${session.id.substring(0, 8)}-${Date.now()}`,
title: `${session.name} — Wraith`, title: `${session.name} — Wraith`,
width: 900, url: `index.html#/detached-session?sessionId=${session.id}&name=${encodeURIComponent(session.name)}&protocol=${session.protocol}`,
height: 600, width: 900, height: 600,
resizable: true, });
center: true, } catch (err) { console.error("Detach window error:", err); }
visible: false,
focus: true,
url: `index.html#/detached-session?sessionId=${session.id}&name=${encodeURIComponent(session.name)}&protocol=${session.protocol}`,
});
wv.once("tauri://created", () => { wv.show(); });
wv.once("tauri://error", (e) => { console.error("Detach window error:", e); });
} }
function closeMenuTab(): void { function closeMenuTab(): void {

View File

@ -367,20 +367,14 @@ function closeHelpMenuDeferred(): void {
async function handleHelpAction(page: string): Promise<void> { async function handleHelpAction(page: string): Promise<void> {
showHelpMenu.value = false; showHelpMenu.value = false;
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow"); try {
const label = `help-${page}-${Date.now()}`; await invoke("open_child_window", {
const wv = new WebviewWindow(label, { label: `help-${page}-${Date.now()}`,
title: `Wraith — Help`, title: "Wraith — Help",
width: 750, url: `index.html#/tool/help?page=${page}`,
height: 600, width: 750, height: 600,
resizable: true, });
center: true, } catch (err) { console.error("Help window error:", err); alert("Window error: " + String(err)); }
visible: false,
focus: true,
url: `index.html#/tool/help?page=${page}`,
});
wv.once("tauri://created", () => { wv.show(); });
wv.once("tauri://error", (e) => { console.error("Help window error:", e); alert("Window error: " + JSON.stringify(e.payload)); });
} }
async function handleToolAction(tool: string): Promise<void> { async function handleToolAction(tool: string): Promise<void> {
@ -394,8 +388,6 @@ async function handleToolAction(tool: string): Promise<void> {
return; return;
} }
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
const toolConfig: Record<string, { title: string; width: number; height: number }> = { const toolConfig: Record<string, { title: string; width: number; height: number }> = {
"network-scanner": { title: "Network Scanner", width: 800, height: 600 }, "network-scanner": { title: "Network Scanner", width: 800, height: 600 },
"port-scanner": { title: "Port Scanner", width: 700, height: 500 }, "port-scanner": { title: "Port Scanner", width: 700, height: 500 },
@ -416,20 +408,14 @@ async function handleToolAction(tool: string): Promise<void> {
const sessionId = activeSessionId.value || ""; const sessionId = activeSessionId.value || "";
// Open tool in a new Tauri window create hidden, show after webview confirms ready try {
const label = `tool-${tool}-${Date.now()}`; await invoke("open_child_window", {
const wv = new WebviewWindow(label, { label: `tool-${tool}-${Date.now()}`,
title: `Wraith — ${config.title}`, title: `Wraith — ${config.title}`,
width: config.width, url: `index.html#/tool/${tool}?sessionId=${sessionId}`,
height: config.height, width: config.width, height: config.height,
resizable: true, });
center: true, } catch (err) { console.error("Tool window error:", err); alert("Tool window error: " + String(err)); }
visible: false,
focus: true,
url: `index.html#/tool/${tool}?sessionId=${sessionId}`,
});
wv.once("tauri://created", () => { wv.show(); });
wv.once("tauri://error", (e) => { console.error("Tool window error:", e); alert("Window error: " + JSON.stringify(e.payload)); });
} }
async function handleFileMenuAction(action: string): Promise<void> { async function handleFileMenuAction(action: string): Promise<void> {
@ -449,23 +435,14 @@ function handleThemeSelect(theme: ThemeDefinition): void {
async function handleOpenFile(entry: FileEntry): Promise<void> { async function handleOpenFile(entry: FileEntry): Promise<void> {
if (!activeSessionId.value) return; if (!activeSessionId.value) return;
try { try {
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
const fileName = entry.path.split("/").pop() || entry.path; const fileName = entry.path.split("/").pop() || entry.path;
const label = `editor-${Date.now()}`;
const sessionId = activeSessionId.value; const sessionId = activeSessionId.value;
await invoke("open_child_window", {
const wv = new WebviewWindow(label, { label: `editor-${Date.now()}`,
title: `${fileName} — Wraith Editor`, title: `${fileName} — Wraith Editor`,
width: 800,
height: 600,
resizable: true,
center: true,
visible: false,
focus: true,
url: `index.html#/tool/editor?sessionId=${sessionId}&path=${encodeURIComponent(entry.path)}`, url: `index.html#/tool/editor?sessionId=${sessionId}&path=${encodeURIComponent(entry.path)}`,
width: 800, height: 600,
}); });
wv.once("tauri://created", () => { wv.show(); });
wv.once("tauri://error", (e) => { console.error("Editor window error:", e); alert("Window error: " + JSON.stringify(e.payload)); });
} catch (err) { console.error("Failed to open editor:", err); } } catch (err) { console.error("Failed to open editor:", err); }
} }