From aea87be4d33820114a5323a3a029a533b61c71d4 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Tue, 13 Jan 2026 13:57:09 +0800 Subject: [PATCH 1/2] feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> --- src/backend/utils/user-data-import.ts | 4 +-- src/ui/desktop/DesktopApp.tsx | 43 ++++++++++++++++++--------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/backend/utils/user-data-import.ts b/src/backend/utils/user-data-import.ts index b06035a1..956f7ce6 100644 --- a/src/backend/utils/user-data-import.ts +++ b/src/backend/utils/user-data-import.ts @@ -211,7 +211,7 @@ class UserDataImport { newHostData, targetUserId, options.userDataKey, - ); + ) as Record; } delete processedHostData.id; @@ -294,7 +294,7 @@ class UserDataImport { newCredentialData, targetUserId, options.userDataKey, - ); + ) as Record; } delete processedCredentialData.id; diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 1ac1d978..034fd4e4 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -13,6 +13,7 @@ import { AdminSettings } from "@/ui/desktop/apps/admin/AdminSettings.tsx"; import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx"; import { NetworkGraphView } from "@/ui/desktop/dashboard/network-graph"; import { Toaster } from "@/components/ui/sonner.tsx"; +import { toast } from "sonner"; import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx"; import { getUserInfo, logoutUser, isElectron } from "@/ui/main-axios.ts"; import { useTheme } from "@/components/theme-provider"; @@ -89,30 +90,44 @@ function AppContent() { useEffect(() => { const path = window.location.pathname; - const match = path.match(/^\/hosts\/([a-zA-Z0-9_-]+)\/terminal$/); - if (match) { - const hostId = match[1]; - - const openTerminalForHost = async () => { + // New format: /terminal/{hostNameOrId} + const terminalMatch = path.match(/^\/terminal\/([a-zA-Z0-9_-]+)$/); + // Legacy format: /hosts/{id}/terminal (backward compatible) + const legacyMatch = path.match(/^\/hosts\/([a-zA-Z0-9_-]+)\/terminal$/); + const hostIdentifier = terminalMatch?.[1] || legacyMatch?.[1]; + + if (hostIdentifier) { + const openTerminal = async () => { try { - const { getSSHHostById } = await import("@/ui/main-axios.ts"); - const host = await getSSHHostById(parseInt(hostId, 10)); + const { getSSHHostById, getSSHHosts } = await import("@/ui/main-axios.ts"); + let host = null; + + // Pure numeric → lookup by ID + if (/^\d+$/.test(hostIdentifier)) { + host = await getSSHHostById(parseInt(hostIdentifier, 10)); + } else { + // Non-numeric → lookup by name (first match) + const hosts = await getSSHHosts(); + host = hosts.find((h: { name?: string }) => h.name === hostIdentifier) || null; + } + if (host) { addTab({ type: "terminal", title: host.name || host.ip, - data: { - host, - initialCommand: "", - }, + data: { host, initialCommand: "" }, }); + // Clean URL to prevent re-opening on refresh + window.history.replaceState({}, "", "/"); + } else { + toast.error(`Host "${hostIdentifier}" not found`); } } catch (error) { - console.error("Failed to open terminal for host:", error); + console.error("Failed to open terminal:", error); + toast.error("Failed to open terminal for host"); } }; - - openTerminalForHost(); + openTerminal(); } }, [addTab]); From 8fc038e59b676d42910715633be5695208148bab Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Tue, 13 Jan 2026 13:58:13 +0800 Subject: [PATCH 2/2] feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 --- .../apps/features/terminal/Terminal.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ui/desktop/apps/features/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx index dcee59c4..89c19b00 100644 --- a/src/ui/desktop/apps/features/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -1349,6 +1349,24 @@ export const Terminal = forwardRef( return true; } + // Ctrl+Alt+ → Ctrl+ remapping for browser-blocked shortcuts + // Browsers intercept Ctrl+W/T/N/Q, so we use Ctrl+Alt as an alternative + if (e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey) { + const key = e.key.toLowerCase(); + const blockedKeys = ["w", "t", "n", "q"]; + if (blockedKeys.includes(key)) { + e.preventDefault(); + e.stopPropagation(); + const ctrlCode = key.charCodeAt(0) - 96; + if (webSocketRef.current?.readyState === 1) { + webSocketRef.current.send( + JSON.stringify({ type: "input", data: String.fromCharCode(ctrlCode) }), + ); + } + return false; + } + } + if (showAutocompleteRef.current) { if (e.key === "Escape") { e.preventDefault();