feat: Sidebar host click should focus existing tab instead of opening a new connection (#1289)

https://github.com/Termix-SSH/Support/issues/1289
This commit is contained in:
LukeGus committed 2026-09-20 18:59:33 -05:00
1 parent f8a8139031
commit afae2889cb
9 files changed
+109 -12

No files matched your search

+14
View File
@@ -153,6 +153,20 @@ export function CustomizeSidebarPanel({
}
/>
</SettingRow>
<SettingRow
label={t("hosts.focusExistingTab")}
description={t("hosts.focusExistingTabDesc")}
>
<FakeSwitch
checked={preferences.display.focusExistingTab}
onChange={(v) =>
update((prev) => ({
...prev,
display: { ...prev.display, focusExistingTab: v },
}))
}
/>
</SettingRow>
<SettingRow
label={t("newUi.sidebar.userProfile.statusColors")}
description={t("newUi.sidebar.userProfile.statusColorsDesc")}
+6 -1
View File
@@ -192,7 +192,11 @@ export function HostsPanel({
onOpenTab: (
host: Host,
type: TabType,
options?: { endpointId?: string; label?: string },
options?: {
endpointId?: string;
label?: string;
forceNewTab?: boolean;
},
) => void;
onEditHost: (host: Host) => void;
hostTree?: HostFolder;
@@ -1077,6 +1081,7 @@ export function HostsPanel({
showTags={sidebarPrefs.display.showTags}
openOnDoubleClick={sidebarPrefs.display.openOnDoubleClick}
showFolderPaths={sidebarPrefs.display.showFolderPaths}
focusExistingTab={sidebarPrefs.display.focusExistingTab}
/>
</div>
)}
+10 -1
View File
@@ -69,6 +69,7 @@ export function FolderItem({
showTags = true,
openOnDoubleClick = false,
showFolderPaths = true,
focusExistingTab = true,
arrangeMode = false,
isDragging = false,
onReorderDrop,
@@ -83,7 +84,11 @@ export function FolderItem({
onOpenTab: (
host: Host,
type: TabType,
options?: { endpointId?: string; label?: string },
options?: {
endpointId?: string;
label?: string;
forceNewTab?: boolean;
},
) => void;
onEditHost?: (host: Host) => void;
onShareHost?: (host: Host) => void;
@@ -118,6 +123,8 @@ export function FolderItem({
openOnDoubleClick?: boolean;
/** When false, nested folders hide the parent-path breadcrumb before their name. */
showFolderPaths?: boolean;
/** When true, clicking a host with an already-open tab focuses it instead of opening a new one. */
focusExistingTab?: boolean;
/** When true (rearranging unlocked), the header can be dragged and its
* top/bottom edges become reorder drop zones. The middle still accepts
* hosts dropped into the folder. */
@@ -342,6 +349,7 @@ export function FolderItem({
showTags={showTags}
openOnDoubleClick={openOnDoubleClick}
showFolderPaths={showFolderPaths}
focusExistingTab={focusExistingTab}
arrangeMode={arrangeMode}
onReorderDrop={onReorderDrop}
onFolderDragStart={onFolderDragStart}
@@ -378,6 +386,7 @@ export function FolderItem({
trayTrigger={trayTrigger}
showTags={showTags}
openOnDoubleClick={openOnDoubleClick}
focusExistingTab={focusExistingTab}
arrangeMode={arrangeMode}
onReorderDrop={
onReorderDrop
+32 -5
View File
@@ -28,6 +28,7 @@ import {
Pin,
Server,
Share2,
SquarePlus,
Terminal,
Trash2,
Users,
@@ -268,6 +269,7 @@ export function HostItem({
trayTrigger = "hover",
showTags = true,
openOnDoubleClick = false,
focusExistingTab = true,
showResourceBars = true,
showStatusStripes = true,
rowActions = "full",
@@ -285,7 +287,7 @@ export function HostItem({
host: Host;
onOpenTab: (
type: TabType,
options?: { endpointId?: string; label?: string },
options?: { endpointId?: string; label?: string; forceNewTab?: boolean },
) => void;
onEditHost?: () => void;
onShareHost?: () => void;
@@ -312,6 +314,8 @@ export function HostItem({
showTags?: boolean;
/** Requires a double click to launch instead of a single click. */
openOnDoubleClick?: boolean;
/** When true, clicking a host with an already-open tab focuses it instead of opening a new one. */
focusExistingTab?: boolean;
/** Preset-driven: hides the CPU/RAM bars without changing density. */
showResourceBars?: boolean;
/** Preset-driven: hides the per-row status color stripe. */
@@ -502,11 +506,14 @@ export function HostItem({
: "terminal";
const openHostTab = (
type: TabType,
options?: { endpointId?: string; label?: string },
options?: { endpointId?: string; label?: string; forceNewTab?: boolean },
) => {
markTabSurfaceUsed(type);
recordHostActionPreference(host.id, type);
onOpenTab(type, options);
onOpenTab(type, {
...options,
forceNewTab: options?.forceNewTab ?? !focusExistingTab,
});
};
// Mirrors getSshActions: the single Web UI entry carries an endpointId only
@@ -848,6 +855,17 @@ export function HostItem({
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
{focusExistingTab && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
openHostTab(defaultAction, { forceNewTab: true });
}}
>
<SquarePlus className="size-3.5 mr-2" />
{t("hosts.openInNewTab")}
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{onEditHost && (
<DropdownMenuItem
@@ -1241,6 +1259,13 @@ export function HostItem({
? "bg-muted/15"
: ""
} ${isMenuOpen ? "bg-muted/50" : ""} ${parentDragOver ? "ring-1 ring-inset ring-accent-brand bg-accent-brand/10" : ""} ${isDragging ? "opacity-40" : ""}`}
onMouseDown={(e) => {
// Middle-click always opens a new tab, matching browser tab behavior.
if (e.button === 1 && !selectionMode && !isTouchOnly) {
e.preventDefault();
openHostTab(defaultAction, { forceNewTab: true });
}
}}
onClick={(e) => {
if (selectionMode) {
onToggleSelect?.();
@@ -1264,12 +1289,14 @@ export function HostItem({
return;
}
if (openOnDoubleClick) return;
openHostTab(defaultAction);
const forceNewTab = e.ctrlKey || e.metaKey;
openHostTab(defaultAction, forceNewTab ? { forceNewTab } : undefined);
}}
onDoubleClick={(e) => {
if (selectionMode || isTouchOnly || !openOnDoubleClick) return;
e.stopPropagation();
openHostTab(defaultAction);
const forceNewTab = e.ctrlKey || e.metaKey;
openHostTab(defaultAction, forceNewTab ? { forceNewTab } : undefined);
}}
>
{/* Status stripe */}
+10 -1
View File
@@ -88,12 +88,17 @@ export function SidebarTree({
showTags = true,
openOnDoubleClick = false,
showFolderPaths = true,
focusExistingTab = true,
}: {
children: (Host | HostFolder)[];
onOpenTab: (
host: Host,
type: TabType,
options?: { endpointId?: string; label?: string },
options?: {
endpointId?: string;
label?: string;
forceNewTab?: boolean;
},
) => void;
onEditHost: (host: Host) => void;
onShareHost?: (host: Host) => void;
@@ -111,6 +116,8 @@ export function SidebarTree({
openOnDoubleClick?: boolean;
/** When false, nested folders hide the parent-path breadcrumb before their name. */
showFolderPaths?: boolean;
/** When true, clicking a host with an already-open tab focuses it instead of opening a new one. */
focusExistingTab?: boolean;
}) {
const { t } = useTranslation();
// Knobs with no other owner come straight from the interface preset; the
@@ -1000,6 +1007,7 @@ export function SidebarTree({
showTags={showTags}
openOnDoubleClick={openOnDoubleClick}
showFolderPaths={showFolderPaths}
focusExistingTab={focusExistingTab}
arrangeMode={arrangeMode}
isDragging={
draggedReorderKey === `folder:${item.path ?? item.name}`
@@ -1076,6 +1084,7 @@ export function SidebarTree({
trayTrigger={trayTrigger}
showTags={showTags}
openOnDoubleClick={openOnDoubleClick}
focusExistingTab={focusExistingTab}
showResourceBars={showResourceBars}
showStatusStripes={showStatusStripes}
rowActions={rowActions}