diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts
index b4467265..d998f752 100644
--- a/src/backend/database/routes/host.ts
+++ b/src/backend/database/routes/host.ts
@@ -739,7 +739,7 @@ router.post(
portKnockSequence: portKnockSequence
? JSON.stringify(portKnockSequence)
: null,
- enableSsh: enableSsh !== false ? 1 : 0,
+ enableSsh: enableSsh ? 1 : 0,
enableRdp: enableRdp ? 1 : 0,
enableVnc: enableVnc ? 1 : 0,
enableTelnet: enableTelnet ? 1 : 0,
@@ -1274,7 +1274,7 @@ router.put(
portKnockSequence: portKnockSequence
? JSON.stringify(portKnockSequence)
: null,
- enableSsh: enableSsh !== false ? 1 : 0,
+ enableSsh: enableSsh ? 1 : 0,
enableRdp: enableRdp ? 1 : 0,
enableVnc: enableVnc ? 1 : 0,
enableTelnet: enableTelnet ? 1 : 0,
diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts
index 62c2bbd6..7a898f2c 100644
--- a/src/types/ui-types.ts
+++ b/src/types/ui-types.ts
@@ -14,6 +14,8 @@ export type Host = {
credentialId?: string;
overrideCredentialUsername?: boolean;
password?: string;
+ hasPassword?: boolean;
+ hasKey?: boolean;
key?: string;
keyPassword?: string;
keyType?: string;
diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx
index 9bc83199..f387c656 100644
--- a/src/ui/AppShell.tsx
+++ b/src/ui/AppShell.tsx
@@ -265,6 +265,11 @@ export function AppShell({
loadHosts();
}, [loadHosts]);
+ useEffect(() => {
+ window.addEventListener("termix:hosts-changed", loadHosts);
+ return () => window.removeEventListener("termix:hosts-changed", loadHosts);
+ }, [loadHosts]);
+
// Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings)
useEffect(() => {
if (allHosts.length === 0) return;
@@ -293,37 +298,29 @@ export function AppShell({
// ─── Tab management ──────────────────────────────────────────────────────
function openTab(host: Host, type: TabType) {
- const same = tabs.filter(
- (t) => t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name,
- );
- if (same.length === 0) {
- const tabId = `${host.name}-${type}`;
- const ref = type === "terminal" ? createRef() : undefined;
- if (ref) terminalRefs.current.set(tabId, ref);
- const tab = { id: tabId, type, label: host.name, host, terminalRef: ref };
- setTabs((prev) => [...prev, tab]);
- setActiveTabId(tab.id);
- return;
- }
const tabId = `${host.name}-${type}-${Date.now()}`;
const ref = type === "terminal" ? createRef() : undefined;
if (ref) terminalRefs.current.set(tabId, ref);
- const tab = {
- id: tabId,
- type,
- label: `${host.name} (${same.length + 1})`,
- host,
- terminalRef: ref,
- };
+
setTabs((prev) => {
- const next = prev.map((t) =>
- t.id === same[0].id && !/\(\d+\)$/.test(t.label)
- ? { ...t, label: `${host.name} (1)`, host }
- : t,
+ const same = prev.filter(
+ (t) =>
+ t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name,
);
- return [...next, tab];
+ const label =
+ same.length === 0 ? host.name : `${host.name} (${same.length + 1})`;
+
+ // Retrofit the first duplicate's label to "(1)" if needed
+ const next =
+ same.length === 1 && !/\(\d+\)$/.test(same[0].label)
+ ? prev.map((t) =>
+ t.id === same[0].id ? { ...t, label: `${host.name} (1)` } : t,
+ )
+ : prev;
+
+ return [...next, { id: tabId, type, label, host, terminalRef: ref }];
});
- setActiveTabId(tab.id);
+ setActiveTabId(tabId);
}
function connectHost(host: Host, preferredType?: TabType) {
@@ -674,36 +671,30 @@ export function AppShell({
) : (
<>
{/* Terminal tabs: always in DOM, absolutely positioned so xterm always has real dimensions */}
- {(() => {
- const activeTab = tabs.find((t) => t.id === activeTabId);
- const nonTerminalActive =
- activeTab && activeTab.type !== "terminal";
- return tabs
- .filter((tab) => tab.type === "terminal")
- .map((tab) => {
- const visible = tab.id === activeTabId;
- return (
-
- {renderTabContent(
- tab,
- openSingletonTab,
- openTab,
- closeTab,
- visible,
- )}
-
- );
- });
- })()}
+ {tabs
+ .filter((tab) => tab.type === "terminal")
+ .map((tab) => {
+ const visible = tab.id === activeTabId;
+ return (
+
+ {renderTabContent(
+ tab,
+ openSingletonTab,
+ openTab,
+ closeTab,
+ visible,
+ )}
+
+ );
+ })}
{/* Non-terminal tabs: absolutely positioned above terminals when active */}
{tabs
.filter((tab) => tab.type !== "terminal")
diff --git a/src/ui/components/slider.tsx b/src/ui/components/slider.tsx
index 231c5a81..794a3f93 100644
--- a/src/ui/components/slider.tsx
+++ b/src/ui/components/slider.tsx
@@ -37,21 +37,23 @@ function Slider({
{Array.from({ length: _values.length }, (_, index) => (
))}
diff --git a/src/ui/features/file-manager/FileManager.tsx b/src/ui/features/file-manager/FileManager.tsx
index 8f799b2f..11c35228 100644
--- a/src/ui/features/file-manager/FileManager.tsx
+++ b/src/ui/features/file-manager/FileManager.tsx
@@ -557,10 +557,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
return false;
}
- if (isLoading && currentLoadingPathRef.current !== path) {
- return false;
- }
-
let resolvedPath = path;
if (path.includes("$") || path.startsWith("~")) {
resolvedPath = await resolveSSHPath(sshSessionId, path);
@@ -711,6 +707,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const navigateTo = useCallback(
(path: string) => {
+ if (sshSessionId) setIsLoading(true);
setCurrentPath(path);
setNavHistory((prev) => {
const next = [...prev.slice(0, navIndex + 1), path];
@@ -718,24 +715,26 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
return next;
});
},
- [navIndex],
+ [navIndex, sshSessionId],
);
const goBack = useCallback(() => {
if (navIndex > 0) {
+ if (sshSessionId) setIsLoading(true);
const newIndex = navIndex - 1;
setNavIndex(newIndex);
setCurrentPath(navHistory[newIndex]);
}
- }, [navIndex, navHistory]);
+ }, [navIndex, navHistory, sshSessionId]);
const goForward = useCallback(() => {
if (navIndex < navHistory.length - 1) {
+ if (sshSessionId) setIsLoading(true);
const newIndex = navIndex + 1;
setNavIndex(newIndex);
setCurrentPath(navHistory[newIndex]);
}
- }, [navIndex, navHistory]);
+ }, [navIndex, navHistory, sshSessionId]);
const goUp = useCallback(() => {
if (currentPath === "/") return;
@@ -1291,6 +1290,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
async function handleFileOpen(file: FileItem) {
if (file.type === "directory") {
+ if (sshSessionId) setIsLoading(true);
setCurrentPath(file.path);
} else if (file.type === "link") {
await handleSymlinkClick(file);
@@ -2599,11 +2599,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
variant="ghost"
size="icon"
onClick={handleRefreshDirectory}
- disabled={isLoading}
className="size-8 rounded-none"
>
@@ -2878,8 +2877,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
onFileOpen={handleFileOpen}
onSelectionChange={setSelection}
currentPath={currentPath}
- isLoading={isLoading}
- isConnected={!!sshSessionId}
onPathChange={navigateTo}
onRefresh={handleRefreshDirectory}
onUpload={handleFilesDropped}
diff --git a/src/ui/features/file-manager/FileManagerGrid.tsx b/src/ui/features/file-manager/FileManagerGrid.tsx
index cddde88f..a25698f4 100644
--- a/src/ui/features/file-manager/FileManagerGrid.tsx
+++ b/src/ui/features/file-manager/FileManagerGrid.tsx
@@ -21,7 +21,6 @@ import {
} from "lucide-react";
import { useTranslation } from "react-i18next";
import type { FileItem } from "@/types/index";
-import { SimpleLoader } from "@/lib/SimpleLoader.tsx";
interface CreateIntent {
id: string;
@@ -66,8 +65,6 @@ interface FileManagerGridProps {
onFileOpen: (file: FileItem) => void;
onSelectionChange: (files: FileItem[]) => void;
currentPath: string;
- isLoading?: boolean;
- isConnected?: boolean;
onPathChange: (path: string) => void;
onRefresh: () => void;
onUpload?: (files: FileList) => void;
@@ -191,8 +188,6 @@ export function FileManagerGrid({
onFileOpen,
onSelectionChange,
currentPath,
- isLoading,
- isConnected,
onPathChange,
onRefresh,
onUpload,
@@ -1240,11 +1235,6 @@ export function FileManagerGrid({
,
document.body,
)}
-
-
);
}
diff --git a/src/ui/features/guacamole/GuacamoleApp.tsx b/src/ui/features/guacamole/GuacamoleApp.tsx
index 00ce371e..17ef8022 100644
--- a/src/ui/features/guacamole/GuacamoleApp.tsx
+++ b/src/ui/features/guacamole/GuacamoleApp.tsx
@@ -83,7 +83,7 @@ const GuacamoleAppInner: React.FC = ({
.then((status) => {
if (status.guacd.status !== "connected") {
setError(
- "Remote desktop service (guacd) is not available. Please ensure guacd is running and accessible.",
+ "Remote desktop service (guacd) is not available. Please ensure guacd is running and accessible and configured properly in admin settings.",
);
return;
}
diff --git a/src/ui/features/terminal/Terminal.tsx b/src/ui/features/terminal/Terminal.tsx
index 46d5d736..45209b1b 100644
--- a/src/ui/features/terminal/Terminal.tsx
+++ b/src/ui/features/terminal/Terminal.tsx
@@ -53,6 +53,46 @@ import { ConnectionLog } from "@/ssh/connection-log/ConnectionLog.tsx";
import { toast } from "sonner";
import { Button } from "@/components/button";
+// Background/foreground per UI theme for "Termix Default" — must match index.css
+const TERMIX_DEFAULT_COLORS: Record<
+ string,
+ { background: string; foreground: string }
+> = {
+ dark: { background: "#0c0d0b", foreground: "#fafafa" },
+ light: { background: "#ffffff", foreground: "#111210" },
+ dracula: { background: "#282a36", foreground: "#f8f8f2" },
+ catppuccin: { background: "#1e1e2e", foreground: "#cdd6f4" },
+ nord: { background: "#2e3440", foreground: "#eceff4" },
+ solarized: { background: "#002b36", foreground: "#839496" },
+ "tokyo-night": { background: "#1a1b26", foreground: "#a9b1d6" },
+ "one-dark": { background: "#282c34", foreground: "#abb2bf" },
+ gruvbox: { background: "#282828", foreground: "#ebdbb2" },
+};
+
+function resolveTermixThemeColors(activeTheme: string, appTheme: string) {
+ if (activeTheme !== "termix") {
+ return (
+ TERMINAL_THEMES[activeTheme]?.colors || TERMINAL_THEMES.termixDark.colors
+ );
+ }
+ let resolvedUiTheme = appTheme;
+ if (appTheme === "system") {
+ resolvedUiTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ }
+ const uiColors =
+ TERMIX_DEFAULT_COLORS[resolvedUiTheme] ?? TERMIX_DEFAULT_COLORS.dark;
+ const base = TERMINAL_THEMES.termixDark.colors;
+ return {
+ ...base,
+ background: uiColors.background,
+ foreground: uiColors.foreground,
+ cursor: uiColors.foreground,
+ cursorAccent: uiColors.background,
+ };
+}
+
interface HostConfig {
id?: number;
instanceId?: string;
@@ -130,27 +170,8 @@ const TerminalInner = forwardRef(
DEFAULT_TERMINAL_CONFIG.theme,
};
- const isDarkMode =
- appTheme === "dark" ||
- appTheme === "dracula" ||
- appTheme === "gentlemansChoice" ||
- appTheme === "midnightEspresso" ||
- appTheme === "catppuccinMocha" ||
- (appTheme === "system" &&
- window.matchMedia("(prefers-color-scheme: dark)").matches);
-
- let themeColors;
const activeTheme = previewTheme || config.theme;
-
- if (activeTheme === "termix") {
- themeColors = isDarkMode
- ? TERMINAL_THEMES.termixDark.colors
- : TERMINAL_THEMES.termixLight.colors;
- } else {
- themeColors =
- TERMINAL_THEMES[activeTheme]?.colors ||
- TERMINAL_THEMES.termixDark.colors;
- }
+ const themeColors = resolveTermixThemeColors(activeTheme, appTheme);
const backgroundColor = themeColors.background;
const fitAddonRef = useRef(null);
const webSocketRef = useRef(null);
@@ -1828,18 +1849,8 @@ const TerminalInner = forwardRef(
...hostConfig.terminalConfig,
};
- let themeColors;
const activeTheme = previewTheme || config.theme;
-
- if (activeTheme === "termix") {
- themeColors = isDarkMode
- ? TERMINAL_THEMES.termixDark.colors
- : TERMINAL_THEMES.termixLight.colors;
- } else {
- themeColors =
- TERMINAL_THEMES[activeTheme]?.colors ||
- TERMINAL_THEMES.termixDark.colors;
- }
+ const themeColors = resolveTermixThemeColors(activeTheme, appTheme);
const fontConfig = TERMINAL_FONTS.find(
(f) => f.value === config.fontFamily,
@@ -1896,13 +1907,7 @@ const TerminalInner = forwardRef(
// Refresh terminal to apply new theme colors to existing buffer content
hardRefresh();
- }, [
- terminal,
- hostConfig.terminalConfig,
- previewTheme,
- isDarkMode,
- isFitted,
- ]);
+ }, [terminal, hostConfig.terminalConfig, previewTheme, appTheme, isFitted]);
useEffect(() => {
if (!terminal || !xtermRef.current) return;
@@ -1917,18 +1922,8 @@ const TerminalInner = forwardRef(
);
const fontFamily = fontConfig?.fallback || TERMINAL_FONTS[0].fallback;
- let themeColors;
const activeTheme = previewTheme || config.theme;
-
- if (activeTheme === "termix") {
- themeColors = isDarkMode
- ? TERMINAL_THEMES.termixDark.colors
- : TERMINAL_THEMES.termixLight.colors;
- } else {
- themeColors =
- TERMINAL_THEMES[activeTheme]?.colors ||
- TERMINAL_THEMES.termixDark.colors;
- }
+ const themeColors = resolveTermixThemeColors(activeTheme, appTheme);
// Set initial options before opening the terminal
terminal.options = {
diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json
index 5303693a..e35e901b 100644
--- a/src/ui/locales/en.json
+++ b/src/ui/locales/en.json
@@ -171,6 +171,7 @@
"upload": "Upload",
"authentication": "Authentication",
"password": "Password",
+ "passwordSaved": "Password saved, type to change",
"key": "Key",
"credential": "Credential",
"none": "None",
@@ -435,11 +436,24 @@
"copiedToClipboard": "Copied to clipboard",
"terminalUrlCopied": "Terminal URL copied",
"fileManagerUrlCopied": "File Manager URL copied",
+ "tunnelUrlCopied": "Tunnel URL copied",
+ "dockerUrlCopied": "Docker URL copied",
+ "serverStatsUrlCopied": "Server Stats URL copied",
+ "rdpUrlCopied": "RDP URL copied",
+ "vncUrlCopied": "VNC URL copied",
+ "telnetUrlCopied": "Telnet URL copied",
"remoteDesktopUrlCopied": "Remote Desktop URL copied",
"cloneHostAction": "Clone Host",
"copyAddress": "Copy Address",
+ "copyLink": "Copy Link",
"copyTerminalUrlAction": "Copy Terminal URL",
"copyFileManagerUrlAction": "Copy File Manager URL",
+ "copyTunnelUrlAction": "Copy Tunnel URL",
+ "copyDockerUrlAction": "Copy Docker URL",
+ "copyServerStatsUrlAction": "Copy Server Stats URL",
+ "copyRdpUrlAction": "Copy RDP URL",
+ "copyVncUrlAction": "Copy VNC URL",
+ "copyTelnetUrlAction": "Copy Telnet URL",
"copyRemoteDesktopUrlAction": "Copy Remote Desktop URL",
"deleteCredentialConfirm": "Delete credential \"{{name}}\"?",
"deletedCredential": "Deleted {{name}}",
diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts
index c0b47ba3..4f46aecb 100644
--- a/src/ui/main-axios.ts
+++ b/src/ui/main-axios.ts
@@ -1107,102 +1107,18 @@ export async function getSSHHosts(): Promise {
export async function createSSHHost(hostData: SSHHostData): Promise {
try {
- const submitData = {
- connectionType: hostData.connectionType || "ssh",
- name: hostData.name || "",
- ip: hostData.ip,
- port: parseInt(hostData.port.toString()) || 22,
- username: hostData.username,
- folder: hostData.folder || "",
- tags: hostData.tags || [],
- pin: Boolean(hostData.pin),
- authType: hostData.authType,
- password:
- hostData.connectionType !== "ssh"
- ? hostData.password || null
- : hostData.authType === "password"
- ? hostData.password
- : null,
- key: hostData.authType === "key" ? hostData.key : null,
- keyPassword: hostData.authType === "key" ? hostData.keyPassword : null,
- keyType: hostData.authType === "key" ? hostData.keyType : null,
- credentialId:
- hostData.authType === "credential" ? hostData.credentialId : null,
- overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
- enableTerminal: Boolean(hostData.enableTerminal),
- enableTunnel: Boolean(hostData.enableTunnel),
- enableFileManager: Boolean(hostData.enableFileManager),
- enableDocker: Boolean(hostData.enableDocker),
- showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
- showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
- showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
- showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
- showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
- defaultPath: hostData.defaultPath || "/",
- tunnelConnections: hostData.tunnelConnections || [],
- jumpHosts: hostData.jumpHosts || [],
- quickActions: hostData.quickActions || [],
- sudoPassword: hostData.sudoPassword || null,
- statsConfig: hostData.statsConfig || null,
- dockerConfig: hostData.dockerConfig || null,
- terminalConfig: hostData.terminalConfig || null,
- forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
- domain: hostData.domain || null,
- security: hostData.security || null,
- ignoreCert: Boolean(hostData.ignoreCert),
- guacamoleConfig: hostData.guacamoleConfig || null,
- notes: hostData.notes || "",
- useSocks5: Boolean(hostData.useSocks5),
- socks5Host: hostData.socks5Host || null,
- socks5Port: hostData.socks5Port || null,
- socks5Username: hostData.socks5Username || null,
- socks5Password: hostData.socks5Password || null,
- socks5ProxyChain: hostData.socks5ProxyChain || null,
- macAddress: hostData.macAddress || null,
- portKnockSequence: hostData.portKnockSequence || null,
- enableSsh: hostData.enableSsh !== false,
- enableRdp: Boolean(hostData.enableRdp),
- enableVnc: Boolean(hostData.enableVnc),
- enableTelnet: Boolean(hostData.enableTelnet),
- sshPort: hostData.sshPort || hostData.port || 22,
- rdpPort: hostData.rdpPort || 3389,
- vncPort: hostData.vncPort || 5900,
- telnetPort: hostData.telnetPort || 23,
- rdpUser: hostData.rdpUser || null,
- rdpPassword: hostData.rdpPassword || null,
- rdpDomain: hostData.rdpDomain || null,
- rdpSecurity: hostData.rdpSecurity || null,
- rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert),
- vncPassword: hostData.vncPassword || null,
- vncUser: hostData.vncUser || null,
- telnetUser: hostData.telnetUser || null,
- telnetPassword: hostData.telnetPassword || null,
- };
-
- if (!submitData.enableTunnel) {
- submitData.tunnelConnections = [];
- }
-
- if (!submitData.enableFileManager) {
- submitData.defaultPath = "";
- }
-
if (hostData.authType === "key" && hostData.key instanceof File) {
const formData = new FormData();
formData.append("key", hostData.key);
-
- const dataWithoutFile = { ...submitData };
- delete dataWithoutFile.key;
+ const dataWithoutFile = { ...hostData, key: undefined };
formData.append("data", JSON.stringify(dataWithoutFile));
-
const response = await sshHostApi.post("/db/host", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
- } else {
- const response = await sshHostApi.post("/db/host", submitData);
- return response.data;
}
+ const response = await sshHostApi.post("/db/host", hostData);
+ return response.data;
} catch (error) {
throw handleApiError(error, "create SSH host");
}
@@ -1213,101 +1129,18 @@ export async function updateSSHHost(
hostData: SSHHostData,
): Promise {
try {
- const submitData = {
- connectionType: hostData.connectionType || "ssh",
- name: hostData.name || "",
- ip: hostData.ip,
- port: parseInt(hostData.port.toString()) || 22,
- username: hostData.username,
- folder: hostData.folder || "",
- tags: hostData.tags || [],
- pin: Boolean(hostData.pin),
- authType: hostData.authType,
- password:
- hostData.connectionType !== "ssh"
- ? hostData.password || null
- : hostData.authType === "password"
- ? hostData.password
- : null,
- key: hostData.authType === "key" ? hostData.key : null,
- keyPassword: hostData.authType === "key" ? hostData.keyPassword : null,
- keyType: hostData.authType === "key" ? hostData.keyType : null,
- credentialId:
- hostData.authType === "credential" ? hostData.credentialId : null,
- overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername),
- enableTerminal: Boolean(hostData.enableTerminal),
- enableTunnel: Boolean(hostData.enableTunnel),
- enableFileManager: Boolean(hostData.enableFileManager),
- enableDocker: Boolean(hostData.enableDocker),
- showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
- showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
- showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
- showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
- showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
- defaultPath: hostData.defaultPath || "/",
- tunnelConnections: hostData.tunnelConnections || [],
- jumpHosts: hostData.jumpHosts || [],
- quickActions: hostData.quickActions || [],
- sudoPassword: hostData.sudoPassword || null,
- statsConfig: hostData.statsConfig || null,
- dockerConfig: hostData.dockerConfig || null,
- terminalConfig: hostData.terminalConfig || null,
- forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
- domain: hostData.domain || null,
- security: hostData.security || null,
- ignoreCert: Boolean(hostData.ignoreCert),
- guacamoleConfig: hostData.guacamoleConfig || null,
- notes: hostData.notes || "",
- useSocks5: Boolean(hostData.useSocks5),
- socks5Host: hostData.socks5Host || null,
- socks5Port: hostData.socks5Port || null,
- socks5Username: hostData.socks5Username || null,
- socks5Password: hostData.socks5Password || null,
- socks5ProxyChain: hostData.socks5ProxyChain || null,
- macAddress: hostData.macAddress || null,
- portKnockSequence: hostData.portKnockSequence || null,
- enableSsh: hostData.enableSsh !== false,
- enableRdp: Boolean(hostData.enableRdp),
- enableVnc: Boolean(hostData.enableVnc),
- enableTelnet: Boolean(hostData.enableTelnet),
- sshPort: hostData.sshPort || hostData.port || 22,
- rdpPort: hostData.rdpPort || 3389,
- vncPort: hostData.vncPort || 5900,
- telnetPort: hostData.telnetPort || 23,
- rdpUser: hostData.rdpUser || null,
- rdpPassword: hostData.rdpPassword || null,
- rdpDomain: hostData.rdpDomain || null,
- rdpSecurity: hostData.rdpSecurity || null,
- rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert),
- vncPassword: hostData.vncPassword || null,
- vncUser: hostData.vncUser || null,
- telnetUser: hostData.telnetUser || null,
- telnetPassword: hostData.telnetPassword || null,
- };
-
- if (!submitData.enableTunnel) {
- submitData.tunnelConnections = [];
- }
- if (!submitData.enableFileManager) {
- submitData.defaultPath = "";
- }
-
if (hostData.authType === "key" && hostData.key instanceof File) {
const formData = new FormData();
formData.append("key", hostData.key);
-
- const dataWithoutFile = { ...submitData };
- delete dataWithoutFile.key;
+ const dataWithoutFile = { ...hostData, key: undefined };
formData.append("data", JSON.stringify(dataWithoutFile));
-
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
- } else {
- const response = await sshHostApi.put(`/db/host/${hostId}`, submitData);
- return response.data;
}
+ const response = await sshHostApi.put(`/db/host/${hostId}`, hostData);
+ return response.data;
} catch (error) {
throw handleApiError(error, "update SSH host");
}
diff --git a/src/ui/shell/tabUtils.tsx b/src/ui/shell/tabUtils.tsx
index c31bc58c..d96f5f7e 100644
--- a/src/ui/shell/tabUtils.tsx
+++ b/src/ui/shell/tabUtils.tsx
@@ -23,6 +23,7 @@ import { TunnelTab } from "@/features/tunnel/TunnelTab";
import { NetworkGraphCard } from "@/dashboard/cards/NetworkGraphCard";
import type { Tab, TabType, Host } from "@/types/ui-types";
import type { SSHHost } from "@/types";
+import { useTabsSafe } from "@/shell/TabContext";
function hostToSSHHost(h: Host): SSHHost {
return {
@@ -109,6 +110,41 @@ export function tabIcon(type: TabType) {
}
}
+function TerminalTabContent({
+ tab,
+ host,
+ label,
+ isVisible,
+ onCloseTab,
+}: {
+ tab: Tab;
+ host: Host;
+ label: string;
+ isVisible: boolean;
+ onCloseTab?: (id: string) => void;
+}) {
+ const { previewTerminalTheme } = useTabsSafe();
+ return (
+
+ onCloseTab?.(tab.id)}
+ previewTheme={previewTerminalTheme}
+ />
+
+ );
+}
+
export function renderTabContent(
tab: Tab,
onOpenSingletonTab?: (type: TabType) => void,
@@ -136,22 +172,13 @@ export function renderTabContent(
/>
);
return (
-
- onCloseTab?.(tab.id)}
- />
-
+
);
case "files":
diff --git a/src/ui/sidebar/HostManager.tsx b/src/ui/sidebar/HostManager.tsx
index 7ab38a69..de051d01 100644
--- a/src/ui/sidebar/HostManager.tsx
+++ b/src/ui/sidebar/HostManager.tsx
@@ -14,6 +14,7 @@ import {
} from "@/lib/terminal-themes";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
+import { PasswordInput } from "@/components/password-input";
import { Slider } from "@/components/slider";
import {
Activity,
@@ -32,6 +33,7 @@ import {
Info,
KeyRound,
LayoutDashboard,
+ Link,
ListChecks,
Lock,
MoreHorizontal,
@@ -60,6 +62,9 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/dropdown-menu";
import { toast } from "sonner";
@@ -98,6 +103,7 @@ import {
import type { SSHHostWithStatus } from "@/main-axios";
import type { Host, Credential } from "@/types/ui-types";
+import { useTabsSafe } from "@/shell/TabContext";
function sshHostToHost(h: SSHHostWithStatus): Host {
const parseJson = (v: any) => {
@@ -125,6 +131,8 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
tags: h.tags ?? [],
authType: h.authType,
password: h.password,
+ hasPassword: !!(h as any).hasPassword || !!h.password,
+ hasKey: !!(h as any).hasKey || !!(typeof h.key === "string" && h.key),
key: typeof h.key === "string" ? h.key : undefined,
keyPassword: h.keyPassword,
keyType: h.keyType,
@@ -132,15 +140,10 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
notes: h.notes,
pin: h.pin ?? false,
macAddress: h.macAddress,
- enableSsh:
- h.enableSsh != null
- ? h.enableSsh
- : h.connectionType === "ssh" || !h.connectionType,
+ enableSsh: h.enableSsh != null ? h.enableSsh : h.connectionType === "ssh",
enableTerminal:
h.enableTerminal ??
- (h.enableSsh != null
- ? h.enableSsh
- : h.connectionType === "ssh" || !h.connectionType),
+ (h.enableSsh != null ? h.enableSsh : h.connectionType === "ssh"),
enableTunnel: h.enableTunnel ?? false,
enableFileManager: h.enableFileManager ?? false,
enableDocker: h.enableDocker ?? false,
@@ -539,7 +542,7 @@ function HostRow({
paddingRight: "8px",
}}
>
- {host.enableTerminal && (
+ {host.enableSsh && host.enableTerminal && (
)}
- {host.enableFileManager && (
+ {host.enableSsh && host.enableFileManager && (
)}
- {host.enableDocker && (
+ {host.enableSsh && host.enableDocker && (
)}
- {host.enableTunnel && (
+ {host.enableSsh && host.enableTunnel && (
)}
- {metricsEnabled && (
+ {host.enableSsh && metricsEnabled && (