e.preventDefault()}
- className="flex items-center bg-background/85 backdrop-blur-sm border border-border shadow-lg rounded-sm px-0.5 py-0.5 gap-0"
>
- {/* Drag handle */}
-
-
-
-
-
- {t("guacamole.toolbar.dragHandle")}
-
-
+ {collapsed ? (
+
+
+
+
+
+
+
+
+
+ {t("guacamole.toolbar.expand")}
+
+
+ ) : (
+
+ {/* Drag handle */}
+
+
+
+
+
+ {t("guacamole.toolbar.dragHandle")}
+
+
- {/* System combos — RDP/VNC only */}
- {isRdpVnc && (
- <>
+ {/* System combos — RDP/VNC only */}
+ {isRdpVnc && (
+ <>
+
+
sendCombo(0xffe3, 0xffe9, 0xffff)}
+ >
+ CAD
+
+
sendCombo(0xff67, 0x006c)}
+ >
+ Win+L
+
+
sendCombo(0xff67)}
+ >
+ Win
+
+ >
+ )}
+
+ {/* Sticky modifiers — RDP/VNC only */}
+ {isRdpVnc && (
+ <>
+
+ {(
+ [
+ [
+ "ctrl",
+ MODIFIER_KEYSYMS.ctrl,
+ t("guacamole.toolbar.ctrl"),
+ ],
+ ["alt", MODIFIER_KEYSYMS.alt, t("guacamole.toolbar.alt")],
+ [
+ "shift",
+ MODIFIER_KEYSYMS.shift,
+ t("guacamole.toolbar.shift"),
+ ],
+ ["win", MODIFIER_KEYSYMS.win, t("guacamole.toolbar.win")],
+ ] as [string, number, string][]
+ ).map(([key, ks, label]) => (
+
+
+
+
+
+ {stickyKeys[ks]
+ ? t("guacamole.toolbar.stickyActive", { key: label })
+ : t("guacamole.toolbar.stickyInactive", { key: label })}
+
+
+ ))}
+ >
+ )}
+
+ {/* Function key toggle */}
sendCombo(0xffe3, 0xffe9, 0xffff)}
+ tooltip={t("guacamole.toolbar.fnToggle")}
+ onClick={() => setShowFKeys((v) => !v)}
+ className={cn(
+ showFKeys &&
+ "bg-primary/15 text-primary border border-primary/30",
+ )}
>
- CAD
+ Fn
-
sendCombo(0xff67, 0x006c)}
- >
- Win+L
-
-
sendCombo(0xff67)}
- >
- Win
-
- >
- )}
- {/* Sticky modifiers — RDP/VNC only */}
- {isRdpVnc && (
- <>
+ {/* F1-F12 row */}
+ {showFKeys &&
+ FKEY_KEYSYMS.map((ks, i) => (
+
sendCombo(ks)}
+ >
+ F{i + 1}
+
+ ))}
+
+ {/* Navigation */}
- {(
- [
- ["ctrl", MODIFIER_KEYSYMS.ctrl, t("guacamole.toolbar.ctrl")],
- ["alt", MODIFIER_KEYSYMS.alt, t("guacamole.toolbar.alt")],
- ["shift", MODIFIER_KEYSYMS.shift, t("guacamole.toolbar.shift")],
- ["win", MODIFIER_KEYSYMS.win, t("guacamole.toolbar.win")],
- ] as [string, number, string][]
- ).map(([key, ks, label]) => (
-
-
-
-
-
- {stickyKeys[ks]
- ? t("guacamole.toolbar.stickyActive", { key: label })
- : t("guacamole.toolbar.stickyInactive", { key: label })}
-
-
- ))}
- >
- )}
-
- {/* Function key toggle */}
-
-
setShowFKeys((v) => !v)}
- className={cn(
- showFKeys && "bg-primary/15 text-primary border border-primary/30",
- )}
- >
- Fn
-
-
- {/* F1-F12 row */}
- {showFKeys &&
- FKEY_KEYSYMS.map((ks, i) => (
sendCombo(ks)}
+ tooltip={t("guacamole.toolbar.esc")}
+ onClick={() => sendCombo(0xff1b)}
>
- F{i + 1}
+ Esc
- ))}
-
- {/* Navigation */}
-
-
sendCombo(0xff1b)}
- >
- Esc
-
-
sendCombo(0xff09)}
- >
- Tab
-
-
sendCombo(0xff50)}
- >
- Home
-
-
sendCombo(0xff57)}
- >
- End
-
-
sendCombo(0xff55)}
- >
- PgUp
-
-
sendCombo(0xff56)}
- >
- PgDn
-
-
- {/* Arrow cluster */}
-
-
-
sendCombo(0xff52)}
+ sendCombo(0xff09)}
>
-
-
+ Tab
+
+
sendCombo(0xff50)}
+ >
+ Home
+
+
sendCombo(0xff57)}
+ >
+ End
+
+
sendCombo(0xff55)}
+ >
+ PgUp
+
+
sendCombo(0xff56)}
+ >
+ PgDn
+
+
+ {/* Arrow cluster */}
+
+
+ sendCombo(0xff52)}
+ >
+
+
+
+
+ sendCombo(0xff51)}
+ >
+
+
+ sendCombo(0xff54)}
+ >
+
+
+ sendCombo(0xff53)}
+ >
+
+
+
+
+
+ {/* Collapse */}
+
+
+
+
+
+
+ {t("guacamole.toolbar.collapse")}
+
+
-
- sendCombo(0xff51)}
- >
-
-
- sendCombo(0xff54)}
- >
-
-
- sendCombo(0xff53)}
- >
-
-
-
-
-
- {/* Session */}
-
-
-
-
-
- {/* Collapse */}
-
-
-
-
-
-
- {t("guacamole.toolbar.collapse")}
-
-
+ )}
);
diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json
index ee172558..fbe9eb3f 100644
--- a/src/ui/locales/en.json
+++ b/src/ui/locales/en.json
@@ -131,6 +131,7 @@
"passwordCopied": "Password copied to clipboard",
"noPasswordAvailable": "No password available",
"failedToCopyPassword": "Failed to copy password",
+ "refreshTab": "Refresh connection",
"openFileManager": "Open File Manager",
"dashboard": "Dashboard",
"networkGraph": "Network Graph",
@@ -293,6 +294,9 @@
"keyFileLoaded": "Key file loaded",
"keyUploadClick": "Click to upload .pem / .key / .ppk",
"clearKey": "Clear key",
+ "keySaved": "SSH key saved",
+ "keyReplaceNotice": "paste a new key below to replace it",
+ "replaceKey": "Replace key",
"forceKeyboardInteractiveLabel": "Force Keyboard Interactive",
"forceKeyboardInteractiveShortDesc": "Force manual password entry even if keys are present",
"terminalAppearance": "Terminal Appearance",
@@ -509,6 +513,7 @@
"hostsTab": "Hosts",
"credentialsTab": "Credentials",
"selectMultiple": "Select multiple",
+ "selectHosts": "Select hosts",
"connectionLabel": "Connection",
"authenticationLabel": "Authentication",
"generateKeyPairTitle": "Generate Key Pair",
diff --git a/src/ui/shell/TabBar.tsx b/src/ui/shell/TabBar.tsx
index 56b6b90f..c868b2ff 100644
--- a/src/ui/shell/TabBar.tsx
+++ b/src/ui/shell/TabBar.tsx
@@ -6,21 +6,25 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/dropdown-menu";
-import { ChevronDown, ChevronUp, X } from "lucide-react";
+import { ChevronDown, ChevronUp, RefreshCw, X } from "lucide-react";
import { tabIcon } from "@/shell/tabUtils";
-import type { Tab } from "@/types/ui-types";
+import type { Tab, TabType } from "@/types/ui-types";
+
+const CONNECTION_TAB_TYPES: TabType[] = ["terminal", "rdp", "vnc", "telnet"];
export function TabBar({
tabs,
activeTabId,
onSetActiveTab,
onCloseTab,
+ onRefreshTab,
onReorderTabs,
}: {
tabs: Tab[];
activeTabId: string;
onSetActiveTab: (id: string) => void;
onCloseTab: (id: string) => void;
+ onRefreshTab: (id: string) => void;
onReorderTabs: (tabs: Tab[]) => void;
}) {
const [open, setOpen] = useState(true);
@@ -217,16 +221,33 @@ export function TabBar({
{tabIcon(tab.type)}
{tab.type !== "dashboard" && tab.label}
{tab.type !== "dashboard" && (
-
);
@@ -272,7 +293,7 @@ export function TabBar({