diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts index 4fc50b68d7e0..ae387154102e 100644 --- a/packages/app/e2e/settings/settings-keybinds.spec.ts +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -208,21 +208,21 @@ test("changing new session keybind works", async ({ page, sdk, gotoSession }) => await keybindButton.click() await expect(keybindButton).toHaveText(/press/i) - await page.keyboard.press(`${modKey}+Shift+KeyN`) + await page.keyboard.press(`${modKey}+Shift+KeyY`) await page.waitForTimeout(100) const newKeybind = await keybindButton.textContent() - expect(newKeybind).toContain("N") + expect(newKeybind).toContain("Y") const stored = await page.evaluate(() => { const raw = localStorage.getItem("settings.v3") return raw ? JSON.parse(raw) : null }) - expect(stored?.keybinds?.["session.new"]).toBe("mod+shift+n") + expect(stored?.keybinds?.["session.new"]).toBe("mod+shift+y") await closeDialog(page, dialog) - await page.keyboard.press(`${modKey}+Shift+N`) + await page.keyboard.press(`${modKey}+Shift+Y`) await page.waitForTimeout(200) const newUrl = page.url() diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 3bdc46391b67..649a8fa49916 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -87,6 +87,9 @@ export type Platform = { /** Read image from clipboard (desktop only) */ readClipboardImage?(): Promise + + /** Spawn a new window (desktop only) */ + spawnWindow?(): Promise } export type DisplayBackend = "auto" | "wayland" diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 39317b8d657e..851153898d9d 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -16,6 +16,7 @@ export const dict = { "command.category.permissions": "Permissions", "command.category.workspace": "Workspace", "command.category.settings": "Settings", + "command.category.window": "Window", "theme.scheme.system": "System", "theme.scheme.light": "Light", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b5a96110f651..30c9291b60d8 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1150,6 +1150,13 @@ export default function Layout(props: ParentProps) { keybind: "mod+shift+t", onSelect: () => cycleTheme(1), }, + { + id: "window.new", + title: language.t("command.window.new"), + category: language.t("command.category.window"), + keybind: "mod+shift+n", + onSelect: () => platform.spawnWindow?.(), + }, ] for (const [id] of availableThemeEntries()) { diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 032343204cc5..5acd38b9808a 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -241,6 +241,12 @@ registerIpcHandlers({ checkUpdate: async () => checkUpdate(), installUpdate: async () => installUpdate(), setBackgroundColor: (color) => setBackgroundColor(color), + spawnWindow: () => { + createMainWindow({ + updaterEnabled: UPDATER_ENABLED, + deepLinks: [], + }) + }, }) function killSidecar() { diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts index d2cfc25241ed..b3e920035495 100644 --- a/packages/desktop-electron/src/main/ipc.ts +++ b/packages/desktop-electron/src/main/ipc.ts @@ -30,6 +30,7 @@ type Deps = { checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> installUpdate: () => Promise | void setBackgroundColor: (color: string) => void + spawnWindow: () => void } export function registerIpcHandlers(deps: Deps) { @@ -60,6 +61,7 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("check-update", () => deps.checkUpdate()) ipcMain.handle("install-update", () => deps.installUpdate()) ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color)) + ipcMain.handle("spawn-window", () => deps.spawnWindow()) ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => { const store = getStore(name) const value = store.get(key) diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts index 296fcb2f1cc1..dcaf4d286688 100644 --- a/packages/desktop-electron/src/preload/index.ts +++ b/packages/desktop-electron/src/preload/index.ts @@ -64,6 +64,7 @@ const api: ElectronAPI = { checkUpdate: () => ipcRenderer.invoke("check-update"), installUpdate: () => ipcRenderer.invoke("install-update"), setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color), + spawnWindow: () => ipcRenderer.invoke("spawn-window"), } contextBridge.exposeInMainWorld("api", api) diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index f8e6d52c7db6..73f5198168be 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -70,4 +70,5 @@ export type ElectronAPI = { checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> installUpdate: () => Promise setBackgroundColor: (color: string) => Promise + spawnWindow: () => Promise } diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index 44f2e6360c35..92253d9359e1 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -238,6 +238,10 @@ const createPlatform = (): Platform => { type: "image/png", }) }, + + spawnWindow: async () => { + await window.api.spawnWindow() + }, } } diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index a843ac8174e5..094310fb749a 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -163,6 +163,13 @@ fn resolve_app_path(app_name: &str) -> Option { } } +#[tauri::command] +#[specta::specta] +fn spawn_window(app: AppHandle) -> Result<(), String> { + let label = uuid::Uuid::new_v4().to_string(); + MainWindow::create(&app, &label).map(|_| ()).map_err(|e| e.to_string()) +} + #[tauri::command] #[specta::specta] fn open_path(_app: AppHandle, path: String, app_name: Option) -> Result<(), String> { @@ -387,7 +394,8 @@ fn make_specta_builder() -> tauri_specta::Builder { check_app_exists, wsl_path, resolve_app_path, - open_path + open_path, + spawn_window ]) .events(tauri_specta::collect_events![ LoadingWindowComplete, @@ -515,7 +523,7 @@ async fn initialize(app: AppHandle) { }; // Create main window immediately - the web app handles its own loading/health gate - MainWindow::create(&app).expect("Failed to create main window"); + MainWindow::create(&app, MainWindow::LABEL).expect("Failed to create main window"); let _ = loading_task.await; diff --git a/packages/desktop/src-tauri/src/windows.rs b/packages/desktop/src-tauri/src/windows.rs index f361cbe38d5c..2d99f405f138 100644 --- a/packages/desktop/src-tauri/src/windows.rs +++ b/packages/desktop/src-tauri/src/windows.rs @@ -36,8 +36,8 @@ impl Deref for MainWindow { impl MainWindow { pub const LABEL: &str = "main"; - pub fn create(app: &AppHandle) -> Result { - if let Some(window) = app.get_webview_window(Self::LABEL) { + pub fn create(app: &AppHandle, label: &str) -> Result { + if let Some(window) = app.get_webview_window(label) { let _ = window.set_focus(); let _ = window.unminimize(); return Ok(Self(window)); @@ -49,7 +49,7 @@ impl MainWindow { .unwrap_or(false); let decorations = use_decorations(); let window_builder = base_window_config( - WebviewWindowBuilder::new(app, Self::LABEL, WebviewUrl::App("/".into())), + WebviewWindowBuilder::new(app, label, WebviewUrl::App("/".into())), app, decorations, ) diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index d434d3b35e89..f9cbdd29bb0a 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -19,6 +19,7 @@ export const commands = { wslPath: (path: string, mode: "windows" | "linux" | null) => __TAURI_INVOKE("wsl_path", { path, mode }), resolveAppPath: (appName: string) => __TAURI_INVOKE("resolve_app_path", { appName }), openPath: (path: string, appName: string | null) => __TAURI_INVOKE("open_path", { path, appName }), + spawnWindow: () => __TAURI_INVOKE("spawn_window"), }; /** Events */ diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 5fe88d501bd9..d016924a308c 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -406,6 +406,9 @@ const createPlatform = (): Platform => { }, "image/png") }) }, + spawnWindow: async () => { + await commands.spawnWindow() + }, } }