* feat: add host list sort

* feat: fixed rdp truncating (taskbar invisible unless resizing window) and improved split screen system

* feat: revamp conneciton persistance system to save to backend with a new connections panel to restore old connections and view current ones. Also added new user profile toggle to reopen all tabs (saves and loads from backend). Added user profile toggle for host tray click vs hover.

* feat: added WOL button, added proper use of BASE_PATH, toggles/buttons in admin/user profile now are always visible regardless of sidebar width, duplicating hosts not adding jumphost/socks5, keepalive internal not mulitplying into seconds causing a keepalive error, and finally guacamole giving 1_0_0 and 1_1_0 errors

* feat: add filter host button, improve alert system UI, save sidebar width to localstorage, and fix host toolbar row overflow (add host going off screen on small sidebar width)

* feat: add pin rail toggle, fix command pallete toggle not working, fixed command pallete toggling when typing in a field, made file manager not uppercase, host manager custom ports not loading, guacd hosts made on >=2.2.1, fixed host tags toggling, added reorder snippet sfeature, made snippet folder clllapse and require confimration toggle work, removed file manager color toggle, and fixed macos not displaying GUI until switching to another app and coming back, and jump host servers failing.

* feat: use blacksmith caching for docker compile and improve keepalive system for ssh to all match the same implementation and use the data from the host config instead of a predefined value

* feat: reset host manager state if the form is left and remove file manager color logic from the removed toggle

* feat: update electron version check to use new ui

* feat: improve duplication system to proplery map all fields
This commit is contained in:
Luke Gustafson
2026-05-28 22:05:25 -04:00
committed by LukeGus
parent b5ab1479ce
commit 7370e8f3df
50 changed files with 3337 additions and 577 deletions
+105 -5
View File
@@ -267,17 +267,32 @@ function RowDivider({
function PaneHeader({
tab,
paneIndex,
isFocused,
}: {
tab: Tab | null;
paneIndex: number;
isFocused: boolean;
}) {
const { t } = useTranslation();
return (
<div className="flex items-center gap-1.5 px-2.5 h-7 shrink-0 bg-sidebar border-b border-border text-xs font-medium text-muted-foreground select-none">
<div
className={`flex items-center gap-1.5 px-2.5 h-7 shrink-0 border-b text-xs font-medium select-none transition-colors ${
isFocused
? "bg-accent-brand/10 border-accent-brand/40 text-accent-brand"
: "bg-sidebar border-border text-muted-foreground"
}`}
>
{isFocused && (
<span className="w-1 h-3.5 rounded-full bg-accent-brand shrink-0" />
)}
{tab ? (
<>
<span className="opacity-60">{tabIcon(tab.type)}</span>
<span className="truncate text-foreground">
<span className={isFocused ? "text-accent-brand" : "opacity-60"}>
{tabIcon(tab.type)}
</span>
<span
className={`truncate ${isFocused ? "text-accent-brand font-semibold" : "text-foreground"}`}
>
{tab.type === "dashboard" ? "Dashboard" : tab.label}
</span>
</>
@@ -309,13 +324,21 @@ const Pane = memo(function Pane({
tab,
paneIndex,
isDragging,
isFocused,
onPaneContentRef,
onPaneClick,
onAssignPane,
}: {
tab: Tab | null;
paneIndex: number;
isDragging: boolean;
isFocused: boolean;
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
onPaneClick?: (paneIndex: number) => void;
onAssignPane?: (paneIndex: number, tabId: string) => void;
}) {
const [isDragOver, setIsDragOver] = useState(false);
const contentRef = useCallback(
(el: HTMLDivElement | null) => {
onPaneContentRef?.(paneIndex, el);
@@ -324,14 +347,37 @@ const Pane = memo(function Pane({
);
return (
<div className="relative flex flex-col w-full h-full min-w-0 min-h-0 overflow-hidden">
<PaneHeader tab={tab} paneIndex={paneIndex} />
<div
className={`relative flex flex-col w-full h-full min-w-0 min-h-0 overflow-hidden transition-colors ${
isFocused ? "ring-1 ring-inset ring-accent-brand/30" : ""
} ${isDragOver ? "ring-2 ring-inset ring-accent-brand" : ""}`}
onClick={() => onPaneClick?.(paneIndex)}
onDragOver={(e) => {
e.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={(e) => {
e.preventDefault();
setIsDragOver(false);
const tabId = e.dataTransfer.getData("text/plain");
if (tabId) onAssignPane?.(paneIndex, tabId);
}}
>
<PaneHeader tab={tab} paneIndex={paneIndex} isFocused={isFocused} />
<div className="flex-1 min-h-0 overflow-hidden relative">
{tab ? (
<div ref={contentRef} className="absolute inset-0" />
) : (
<EmptyPane />
)}
{isDragOver && (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-accent-brand/10 border-2 border-dashed border-accent-brand pointer-events-none">
<span className="text-xs font-medium text-accent-brand">
Drop to assign
</span>
</div>
)}
</div>
{isDragging && (
<div className="absolute inset-0 z-10" style={{ cursor: "inherit" }} />
@@ -350,9 +396,12 @@ const Row = memo(function Row({
paneTabIds,
tabs,
isDragging,
focusedPaneIndex,
onColDivider,
onColDividerTouch,
onPaneContentRef,
onPaneClick,
onAssignPane,
}: {
rowIdx: number;
paneIndices: number[];
@@ -361,6 +410,7 @@ const Row = memo(function Row({
paneTabIds: (string | null)[];
tabs: Tab[];
isDragging: boolean;
focusedPaneIndex: number | null;
onColDivider: (e: React.MouseEvent, rowIdx: number, colIdx: number) => void;
onColDividerTouch: (
e: React.TouchEvent,
@@ -368,6 +418,8 @@ const Row = memo(function Row({
colIdx: number,
) => void;
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
onPaneClick?: (paneIndex: number) => void;
onAssignPane?: (paneIndex: number, tabId: string) => void;
}) {
const widths = colWidths ?? [];
return (
@@ -386,7 +438,10 @@ const Row = memo(function Row({
tab={tab}
paneIndex={pIdx}
isDragging={isDragging}
isFocused={focusedPaneIndex === pIdx}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
{ci < paneIndices.length - 1 && (
@@ -408,14 +463,20 @@ export const SplitView = memo(function SplitView({
tabs,
paneTabIds,
splitMode,
focusedPaneIndex,
onTerminalResize,
onPaneContentRef,
onPaneClick,
onAssignPane,
}: {
tabs: Tab[];
paneTabIds: (string | null)[];
splitMode: SplitMode;
focusedPaneIndex?: number | null;
onTerminalResize?: () => void;
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
onPaneClick?: (paneIndex: number) => void;
onAssignPane?: (paneIndex: number, tabId: string) => void;
}) {
const {
rowSizes,
@@ -464,9 +525,12 @@ export const SplitView = memo(function SplitView({
paneTabIds={paneTabIds}
tabs={tabs}
isDragging={isDragging}
focusedPaneIndex={focusedPaneIndex ?? null}
onColDivider={onColDivider}
onColDividerTouch={onColDividerTouch}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
)}
@@ -480,7 +544,10 @@ export const SplitView = memo(function SplitView({
tab={tab(0)}
paneIndex={0}
isDragging={isDragging}
isFocused={focusedPaneIndex === 0}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
<ColDivider
@@ -496,7 +563,10 @@ export const SplitView = memo(function SplitView({
tab={tab(1)}
paneIndex={1}
isDragging={isDragging}
isFocused={focusedPaneIndex === 1}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
<RowDivider
@@ -511,7 +581,10 @@ export const SplitView = memo(function SplitView({
tab={tab(2)}
paneIndex={2}
isDragging={isDragging}
isFocused={focusedPaneIndex === 2}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
</div>
@@ -532,7 +605,10 @@ export const SplitView = memo(function SplitView({
tab={tab(0)}
paneIndex={0}
isDragging={isDragging}
isFocused={focusedPaneIndex === 0}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
<ColDivider
@@ -544,7 +620,10 @@ export const SplitView = memo(function SplitView({
tab={tab(1)}
paneIndex={1}
isDragging={isDragging}
isFocused={focusedPaneIndex === 1}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
</div>
@@ -557,7 +636,10 @@ export const SplitView = memo(function SplitView({
tab={tab(2)}
paneIndex={2}
isDragging={isDragging}
isFocused={focusedPaneIndex === 2}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
</div>
@@ -573,9 +655,12 @@ export const SplitView = memo(function SplitView({
paneTabIds={paneTabIds}
tabs={tabs}
isDragging={isDragging}
focusedPaneIndex={focusedPaneIndex ?? null}
onColDivider={onColDivider}
onColDividerTouch={onColDividerTouch}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
<RowDivider
onMouseDown={(e) => onRowDivider(e, 0)}
@@ -589,9 +674,12 @@ export const SplitView = memo(function SplitView({
paneTabIds={paneTabIds}
tabs={tabs}
isDragging={isDragging}
focusedPaneIndex={focusedPaneIndex ?? null}
onColDivider={onColDivider}
onColDividerTouch={onColDividerTouch}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
)}
@@ -606,9 +694,12 @@ export const SplitView = memo(function SplitView({
paneTabIds={paneTabIds}
tabs={tabs}
isDragging={isDragging}
focusedPaneIndex={focusedPaneIndex ?? null}
onColDivider={onColDivider}
onColDividerTouch={onColDividerTouch}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
<RowDivider
onMouseDown={(e) => onRowDivider(e, 0)}
@@ -622,9 +713,12 @@ export const SplitView = memo(function SplitView({
paneTabIds={paneTabIds}
tabs={tabs}
isDragging={isDragging}
focusedPaneIndex={focusedPaneIndex ?? null}
onColDivider={onColDivider}
onColDividerTouch={onColDividerTouch}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
)}
@@ -639,9 +733,12 @@ export const SplitView = memo(function SplitView({
paneTabIds={paneTabIds}
tabs={tabs}
isDragging={isDragging}
focusedPaneIndex={focusedPaneIndex ?? null}
onColDivider={onColDivider}
onColDividerTouch={onColDividerTouch}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
<RowDivider
onMouseDown={(e) => onRowDivider(e, 0)}
@@ -655,9 +752,12 @@ export const SplitView = memo(function SplitView({
paneTabIds={paneTabIds}
tabs={tabs}
isDragging={isDragging}
focusedPaneIndex={focusedPaneIndex ?? null}
onColDivider={onColDivider}
onColDividerTouch={onColDividerTouch}
onPaneContentRef={onPaneContentRef}
onPaneClick={onPaneClick}
onAssignPane={onAssignPane}
/>
</div>
)}
+170 -4
View File
@@ -1,4 +1,5 @@
import { useRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/button";
import { Separator } from "@/components/separator";
import {
@@ -6,31 +7,57 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/dropdown-menu";
import { ChevronDown, ChevronUp, RefreshCw, X } from "lucide-react";
import {
ChevronDown,
ChevronUp,
RefreshCw,
X,
LayoutPanelLeft,
Plus,
Minus,
} from "lucide-react";
import { tabIcon } from "@/shell/tabUtils";
import type { Tab, TabType } from "@/types/ui-types";
import type { Tab, TabType, SplitMode } from "@/types/ui-types";
import { SPLIT_MODES, PANE_COUNTS } from "@/lib/theme";
const CONNECTION_TAB_TYPES: TabType[] = ["terminal", "rdp", "vnc", "telnet"];
export function TabBar({
tabs,
activeTabId,
splitMode,
paneTabIds,
focusedPaneIndex,
onSetActiveTab,
onCloseTab,
onRefreshTab,
onReorderTabs,
onSplitTab,
onAddToSplit,
onRemoveFromSplit,
}: {
tabs: Tab[];
activeTabId: string;
splitMode: SplitMode;
paneTabIds: (string | null)[];
focusedPaneIndex: number | null;
onSetActiveTab: (id: string) => void;
onCloseTab: (id: string) => void;
onRefreshTab: (id: string) => void;
onReorderTabs: (tabs: Tab[]) => void;
onSplitTab: (tabId: string, mode: SplitMode) => void;
onAddToSplit: (tabId: string) => void;
onRemoveFromSplit: (tabId: string) => void;
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(true);
const [dragTabId, setDragTabId] = useState<string | null>(null);
const [dragTargetIndex, setDragTargetIndex] = useState<number | null>(null);
const [dragPos, setDragPos] = useState<{ x: number; y: number } | null>(null);
const [contextTabId, setContextTabId] = useState<string | null>(null);
const [contextPos, setContextPos] = useState<{ x: number; y: number } | null>(
null,
);
const tabBarRef = useRef<HTMLDivElement>(null);
const tabEls = useRef<Map<string, HTMLDivElement>>(new Map());
@@ -49,6 +76,9 @@ export function TabBar({
const dragTargetRef = useRef<number | null>(null);
const didDrag = useRef(false);
const isSplit = splitMode !== "none";
const paneCount = PANE_COUNTS[splitMode];
useEffect(() => {
const el = tabBarRef.current;
if (!el) return;
@@ -123,6 +153,18 @@ export function TabBar({
};
}, [dragTabId, tabs, onReorderTabs]);
useEffect(() => {
if (!contextTabId) return;
function onDown(e: MouseEvent) {
if (!(e.target as HTMLElement).closest("[data-context-menu]")) {
setContextTabId(null);
setContextPos(null);
}
}
window.addEventListener("mousedown", onDown);
return () => window.removeEventListener("mousedown", onDown);
}, [contextTabId]);
const dragIdx = tabs.findIndex((t) => t.id === dragTabId);
const target = dragTargetIndex ?? dragIdx;
@@ -138,6 +180,9 @@ export function TabBar({
{tabs.map((tab, index) => {
const active = tab.id === activeTabId;
const isDragging = dragTabId === tab.id;
const paneIdx = paneTabIds.indexOf(tab.id);
const isInPane = paneIdx !== -1;
const isFocusedPane = isInPane && paneIdx === focusedPaneIndex;
let translateX = 0;
if (
dragTabId &&
@@ -153,6 +198,10 @@ export function TabBar({
else if (dragIdx > target && index < dragIdx && index >= target)
translateX = draggedWidth;
}
const showFocusIndicator = isFocusedPane && isSplit;
const showInPaneIndicator = isInPane && isSplit && !isFocusedPane;
return (
<div
key={tab.id}
@@ -160,6 +209,12 @@ export function TabBar({
if (el) tabEls.current.set(tab.id, el);
else tabEls.current.delete(tab.id);
}}
draggable={isSplit && tab.type !== "dashboard"}
onDragStart={(e) => {
if (!isSplit || tab.type === "dashboard") return;
e.dataTransfer.setData("text/plain", tab.id);
e.dataTransfer.effectAllowed = "move";
}}
onClick={() =>
!dragTabId && !didDrag.current && onSetActiveTab(tab.id)
}
@@ -169,6 +224,12 @@ export function TabBar({
onCloseTab(tab.id);
}
}}
onContextMenu={(e) => {
if (tab.type === "dashboard") return;
e.preventDefault();
setContextTabId(tab.id);
setContextPos({ x: e.clientX, y: e.clientY });
}}
onPointerDown={(e) => {
if (e.button !== 0 || tab.type === "dashboard") return;
e.preventDefault();
@@ -210,7 +271,7 @@ export function TabBar({
: "grab",
userSelect: "none",
}}
className={`group/tab flex items-center gap-2 shrink-0 transition-colors border-r border-border text-sm
className={`group/tab relative flex items-center gap-2 shrink-0 transition-colors border-r border-border text-sm
${index === 0 && tab.type !== "dashboard" ? "border-l border-border" : ""}
${
tab.type === "dashboard"
@@ -218,6 +279,14 @@ export function TabBar({
: `px-2.5 md:px-4 font-medium ${active ? "border-b-2 border-b-accent-brand bg-surface text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-surface"}`
}`}
>
{/* Focused-pane indicator: brand accent bottom border overlay */}
{showFocusIndicator && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent-brand/70 z-10" />
)}
{/* In-pane (not focused) indicator: subtle dot */}
{showInPaneIndicator && (
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-muted-foreground/40 z-10" />
)}
{tabIcon(tab.type)}
{tab.type !== "dashboard" && tab.label}
{tab.type !== "dashboard" && (
@@ -312,7 +381,7 @@ export function TabBar({
<div className="flex items-center gap-2 flex-1 min-w-0">
{tabIcon(tab.type)}
<span className="truncate">
{tab.type === "dashboard" ? "Dashboard" : tab.label}
{tab.type === "dashboard" ? t("nav.dashboard") : tab.label}
</span>
</div>
{tab.type !== "dashboard" && (
@@ -351,6 +420,103 @@ export function TabBar({
<ChevronDown className="size-3.5" />
</button>
)}
{/* Right-click context menu */}
{contextTabId &&
contextPos &&
(() => {
const ctxTab = tabs.find((t) => t.id === contextTabId);
if (!ctxTab) return null;
const isInPane = paneTabIds.indexOf(contextTabId) !== -1;
const hasEmptySlot =
isSplit && paneTabIds.slice(0, paneCount).some((p) => p === null);
return (
<div
data-context-menu
style={{
position: "fixed",
left: contextPos.x,
top: contextPos.y,
zIndex: 10000,
}}
className="bg-popover border border-border shadow-lg py-1 min-w-[180px]"
>
<div className="px-2 py-1 text-xs font-semibold text-muted-foreground truncate max-w-[200px]">
{ctxTab.label}
</div>
<div className="h-px bg-border my-1" />
{CONNECTION_TAB_TYPES.includes(ctxTab.type) && (
<button
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
onClick={() => {
onRefreshTab(contextTabId);
setContextTabId(null);
}}
>
<RefreshCw className="size-3" />
Refresh connection
</button>
)}
<div className="h-px bg-border my-1" />
{/* Split submenu */}
<div className="px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
Split
</div>
{SPLIT_MODES.filter((m) => m.id !== "none").map((mode) => (
<button
key={mode.id}
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
onClick={() => {
onSplitTab(contextTabId, mode.id);
setContextTabId(null);
}}
>
<LayoutPanelLeft className="size-3 text-muted-foreground" />
{mode.label}
</button>
))}
{isSplit && (
<>
<div className="h-px bg-border my-1" />
{isInPane ? (
<button
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground text-muted-foreground"
onClick={() => {
onRemoveFromSplit(contextTabId);
setContextTabId(null);
}}
>
<Minus className="size-3" />
Remove from split
</button>
) : hasEmptySlot ? (
<button
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
onClick={() => {
onAddToSplit(contextTabId);
setContextTabId(null);
}}
>
<Plus className="size-3" />
Add to split
</button>
) : null}
</>
)}
<div className="h-px bg-border my-1" />
<button
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground text-destructive"
onClick={() => {
onCloseTab(contextTabId);
setContextTabId(null);
}}
>
<X className="size-3" />
Close tab
</button>
</div>
);
})()}
</div>
);
}
+3 -98
View File
@@ -40,9 +40,6 @@ interface TabContextType {
const TabContext = createContext<TabContextType | undefined>(undefined);
type ElectronWindow = Window & {
electronAPI?: unknown;
};
export function useTabs() {
const context = useContext(TabContext);
@@ -79,111 +76,19 @@ interface TabProviderProps {
export function clearTermixSessionStorage() {
localStorage.removeItem("termix_tabs");
localStorage.removeItem("termix_currentTab");
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith("termix_session_")) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((k) => localStorage.removeItem(k));
}
export function TabProvider({ children }: TabProviderProps) {
const { t } = useTranslation();
const [tabs, setTabs] = useState<Tab[]>(() => {
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isElectron =
typeof window !== "undefined" && !!(window as ElectronWindow).electronAPI;
const persistenceEnabled =
localStorage.getItem("enableTerminalSessionPersistence") !== "false";
const shouldRestore = isMobile || isElectron || persistenceEnabled;
if (!shouldRestore) {
return [{ id: 1, type: "home", title: "Home" }];
}
try {
const saved = localStorage.getItem("termix_tabs");
if (saved) {
const parsed = JSON.parse(saved) as Tab[];
const restored: Tab[] = [{ id: 1, type: "home", title: "Home" }];
let maxId = 1;
for (const tab of parsed) {
if (tab.type === "home") continue;
const restoredTab: Tab = {
...tab,
instanceId: tab.instanceId,
terminalRef:
tab.type === "terminal"
? React.createRef<TerminalRefHandle>()
: undefined,
hostConfig: tab.hostConfig
? {
...tab.hostConfig,
instanceId: tab.instanceId,
}
: undefined,
};
restored.push(restoredTab);
if (tab.id > maxId) maxId = tab.id;
}
if (restored.length > 1) return restored;
}
} catch {
/* ignore corrupt data */
}
return [{ id: 1, type: "home", title: "Home" }];
});
const [currentTab, setCurrentTab] = useState<number>(() => {
try {
const saved = localStorage.getItem("termix_currentTab");
if (saved) {
const parsed = parseInt(saved, 10);
if (parsed && tabs.some((t) => t.id === parsed)) return parsed;
}
} catch {
/* ignore */
}
return 1;
});
const [tabs, setTabs] = useState<Tab[]>([{ id: 1, type: "home", title: "Home" }]);
const [currentTab, setCurrentTab] = useState<number>(1);
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
const [previewTerminalTheme, setPreviewTerminalTheme] = useState<
string | null
>(null);
const [initialMaxId] = useState(() => {
let maxId = 1;
tabs.forEach((tab) => {
if (tab.id > maxId) maxId = tab.id;
});
return maxId + 1;
});
const [initialMaxId] = useState(2);
const nextTabId = useRef(initialMaxId);
useEffect(() => {
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isElectron =
typeof window !== "undefined" && !!(window as ElectronWindow).electronAPI;
const persistenceEnabled =
localStorage.getItem("enableTerminalSessionPersistence") !== "false";
const shouldSave = isMobile || isElectron || persistenceEnabled;
if (shouldSave) {
const serializable = tabs
.filter((t) => t.type !== "home")
.map((tab) => {
const rest = { ...tab };
delete rest.terminalRef;
return rest;
});
localStorage.setItem("termix_tabs", JSON.stringify(serializable));
localStorage.setItem("termix_currentTab", String(currentTab));
} else {
localStorage.removeItem("termix_tabs");
localStorage.removeItem("termix_currentTab");
}
}, [tabs, currentTab]);
// Safety net: if currentTab points to a tab that no longer exists, fall back to home
useEffect(() => {
if (tabs.length > 0 && !tabs.some((t) => t.id === currentTab)) {
+2 -1
View File
@@ -132,7 +132,8 @@ function TerminalTabContent({
{
...hostToSSHHost(host),
sshPort: host.sshPort ?? host.port,
instanceId: tab.id,
instanceId: tab.instanceId ?? tab.id,
restoredSessionId: tab.restoredSessionId ?? null,
} as any
}
isVisible={isVisible}