mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
release-2.4.1 (#921)
* chore(deps-dev): bump the dev-minor-updates group across 1 directory with 12 updates (#910) Bumps the dev-minor-updates group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.1` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.3.6` | `1.4.1` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.1` | | [cytoscape](https://github.com/cytoscape/cytoscape.js) | `3.33.4` | `3.34.0` | | [electron](https://github.com/electron/electron) | `42.2.0` | `42.4.1` | | [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) | `26.8.1` | `26.15.3` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.16.0` | `1.20.0` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.4.3` | `1.6.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.79.0` | | [sharp](https://github.com/lovell/sharp) | `0.34.5` | `0.35.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.61.1` | Updates `@radix-ui/react-select` from 2.2.6 to 2.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-slider` from 1.3.6 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot) Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `cytoscape` from 3.33.4 to 3.34.0 - [Release notes](https://github.com/cytoscape/cytoscape.js/releases) - [Commits](https://github.com/cytoscape/cytoscape.js/compare/v3.33.4...v3.34.0) Updates `electron` from 42.2.0 to 42.4.1 - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v42.2.0...v42.4.1) Updates `electron-builder` from 26.8.1 to 26.15.3 - [Release notes](https://github.com/electron-userland/electron-builder/releases) - [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md) - [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.15.3/packages/electron-builder) Updates `lucide-react` from 1.16.0 to 1.20.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react) Updates `radix-ui` from 1.4.3 to 1.6.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui) Updates `react-hook-form` from 7.76.1 to 7.79.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.79.0) Updates `sharp` from 0.34.5 to 0.35.1 - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.1) Updates `typescript-eslint` from 8.60.0 to 8.61.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slider" dependency-version: 1.4.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slot" dependency-version: 1.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: cytoscape dependency-version: 3.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron dependency-version: 42.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron-builder dependency-version: 26.15.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: lucide-react dependency-version: 1.18.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: radix-ui dependency-version: 1.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: react-hook-form dependency-version: 7.79.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: sharp dependency-version: 0.35.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: typescript-eslint dependency-version: 8.61.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump node in /docker in the docker-major-updates group (#914) Bumps the docker-major-updates group in /docker with 1 update: node. Updates `node` from 24-slim to 26-slim --- updated-dependencies: - dependency-name: node dependency-version: 26-slim dependency-type: direct:production dependency-group: docker-major-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ci(deps): bump the github-actions group with 2 updates (#915) Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/checkout` from 5 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) Updates `actions/setup-node` from 4 to 6 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the prod-minor-updates group across 1 directory with 5 updates (#916) Bumps the prod-minor-updates group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.17.0` | `1.18.0` | | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `12.10.0` | `12.11.1` | | [body-parser](https://github.com/expressjs/body-parser) | `2.2.2` | `2.3.0` | | [multer](https://github.com/expressjs/multer) | `2.1.1` | `2.2.0` | | [undici](https://github.com/nodejs/undici) | `8.4.0` | `8.5.0` | Updates `axios` from 1.17.0 to 1.18.0 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0) Updates `better-sqlite3` from 12.10.0 to 12.11.1 - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.10.0...v12.11.1) Updates `body-parser` from 2.2.2 to 2.3.0 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0) Updates `multer` from 2.1.1 to 2.2.0 - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.1.1...v2.2.0) Updates `undici` from 8.4.0 to 8.5.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v8.4.0...v8.5.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: better-sqlite3 dependency-version: 12.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: body-parser dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: multer dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: undici dependency-version: 8.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: add unlink button for dual auth * fix: general bug fixes * fix: general qol/small feature additions * fix: 100mb max upload size * fix: terminal syntax not handling carriage returns or shell prompt lines properly * feat: continued general improvements * fix: terminal outputting success right after folder path * chore: update release notes * feat: continued fixes and improvements * chore: lint, format, and bump version to 2.4.1 * chore: sync Crowdin translations for 2.4.1 * fix: rebase dev branch onto main before Crowdin download * fix: use merge instead of rebase to sync main before Crowdin --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
+272
-5
@@ -36,6 +36,7 @@ import type {
|
||||
FontSizeId,
|
||||
} from "@/types/ui-types";
|
||||
import { applyAccentColor, applyFontSize, PANE_COUNTS } from "@/lib/theme";
|
||||
import { globalShortcutHandler } from "@/lib/global-shortcut-handler";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import {
|
||||
getSSHHosts,
|
||||
@@ -80,7 +81,7 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
enableSsh: h.enableSsh ?? (h.connectionType === "ssh" || !h.connectionType),
|
||||
enableTerminal: h.enableTerminal ?? true,
|
||||
enableTunnel: h.enableTunnel ?? false,
|
||||
enableFileManager: h.enableFileManager ?? false,
|
||||
enableFileManager: h.enableFileManager ?? true,
|
||||
enableDocker: h.enableDocker ?? false,
|
||||
enableProxmox: h.enableProxmox ?? false,
|
||||
enableTmuxMonitor: h.enableTmuxMonitor ?? false, // --- tmux-monitor ---
|
||||
@@ -108,6 +109,9 @@ function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
socks5Username: h.socks5Username,
|
||||
socks5Password: h.socks5Password,
|
||||
socks5ProxyChain: h.socks5ProxyChain ?? [],
|
||||
statsConfig: (typeof h.statsConfig === "string"
|
||||
? JSON.parse(h.statsConfig)
|
||||
: h.statsConfig) as Host["statsConfig"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,9 +165,11 @@ export { tabIcon, renderTabContent } from "@/shell/tabUtils";
|
||||
export function AppShell({
|
||||
username,
|
||||
onLogout,
|
||||
onChangeServer,
|
||||
}: {
|
||||
username: string;
|
||||
onLogout: () => void;
|
||||
onChangeServer?: () => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { setTheme } = useTheme();
|
||||
@@ -193,6 +199,9 @@ export function AppShell({
|
||||
JSON.parse(localStorage.getItem("termix_paneTabIds") ?? "null") ??
|
||||
Array(6).fill(null),
|
||||
);
|
||||
useEffect(() => {
|
||||
paneTabIdsRef.current = paneTabIds;
|
||||
}, [paneTabIds]);
|
||||
const [focusedPaneIndex, setFocusedPaneIndex] = useState<number | null>(null);
|
||||
const [realHostTree, setRealHostTree] = useState<HostFolder | null>(null);
|
||||
const [hostsLoading, setHostsLoading] = useState(true);
|
||||
@@ -204,7 +213,6 @@ export function AppShell({
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [railView, setRailView] = useState<RailView>("hosts");
|
||||
const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||
const saved = localStorage.getItem("termix_sidebarWidth");
|
||||
return saved ? parseInt(saved, 10) : 291;
|
||||
@@ -243,6 +251,26 @@ export function AppShell({
|
||||
}, []);
|
||||
|
||||
const lastShiftTime = useRef(0);
|
||||
const tabsRef = useRef(tabs);
|
||||
const activeTabIdRef = useRef(activeTabId);
|
||||
const splitModeRef = useRef(splitMode);
|
||||
const focusedPaneIndexRef = useRef<number | null>(null);
|
||||
const paneContentElsRef = useRef<(HTMLDivElement | null)[]>(
|
||||
Array(6).fill(null),
|
||||
);
|
||||
const paneTabIdsRef = useRef<(string | null)[]>(Array(6).fill(null));
|
||||
useEffect(() => {
|
||||
tabsRef.current = tabs;
|
||||
}, [tabs]);
|
||||
useEffect(() => {
|
||||
activeTabIdRef.current = activeTabId;
|
||||
}, [activeTabId]);
|
||||
useEffect(() => {
|
||||
splitModeRef.current = splitMode;
|
||||
}, [splitMode]);
|
||||
useEffect(() => {
|
||||
focusedPaneIndexRef.current = focusedPaneIndex;
|
||||
}, [focusedPaneIndex]);
|
||||
const [commandPaletteShortcutEnabled, setCommandPaletteShortcutEnabled] =
|
||||
useState<boolean>(() => {
|
||||
const v = localStorage.getItem("commandPaletteShortcutEnabled");
|
||||
@@ -254,6 +282,9 @@ export function AppShell({
|
||||
const [paneContentEls, setPaneContentEls] = useState<
|
||||
(HTMLDivElement | null)[]
|
||||
>(Array(6).fill(null));
|
||||
useEffect(() => {
|
||||
paneContentElsRef.current = paneContentEls;
|
||||
}, [paneContentEls]);
|
||||
|
||||
// Stable per-tab DOM nodes — created once per tab, never destroyed while the tab lives.
|
||||
// We always portal each tab's content into its own node, then move that node between
|
||||
@@ -314,6 +345,174 @@ export function AppShell({
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [commandPaletteShortcutEnabled]);
|
||||
|
||||
// Split-screen and tab navigation hotkeys
|
||||
// Also registered in globalShortcutHandler so xterm can invoke directly
|
||||
// without going through synthetic DOM events (which are unreliable).
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Ctrl+Shift+\ — toggle 2-way split (side by side)
|
||||
if (e.ctrlKey && e.shiftKey && !e.altKey && e.code === "Backslash") {
|
||||
e.preventDefault();
|
||||
if (splitModeRef.current !== "none") {
|
||||
splitModeRef.current = "none";
|
||||
setSplitMode("none");
|
||||
setPaneTabIds(Array(6).fill(null));
|
||||
} else {
|
||||
const mode = "2-way";
|
||||
splitModeRef.current = mode;
|
||||
const currentTabs = tabsRef.current;
|
||||
const currentActiveId = activeTabIdRef.current;
|
||||
const count = PANE_COUNTS[mode];
|
||||
const next: (string | null)[] = Array(6).fill(null);
|
||||
next[0] = currentActiveId;
|
||||
let slot = 1;
|
||||
for (const tab of currentTabs) {
|
||||
if (slot >= count) break;
|
||||
if (tab.id !== currentActiveId && tab.type !== "dashboard") {
|
||||
next[slot] = tab.id;
|
||||
slot++;
|
||||
}
|
||||
}
|
||||
setSplitMode(mode);
|
||||
setPaneTabIds(next);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Shift+- — toggle 3-way-horizontal split (top/bottom)
|
||||
if (e.ctrlKey && e.shiftKey && !e.altKey && e.code === "Minus") {
|
||||
e.preventDefault();
|
||||
if (splitModeRef.current !== "none") {
|
||||
splitModeRef.current = "none";
|
||||
setSplitMode("none");
|
||||
setPaneTabIds(Array(6).fill(null));
|
||||
} else {
|
||||
const mode = "3-way-horizontal";
|
||||
splitModeRef.current = mode;
|
||||
const currentTabs = tabsRef.current;
|
||||
const currentActiveId = activeTabIdRef.current;
|
||||
const count = PANE_COUNTS[mode];
|
||||
const next: (string | null)[] = Array(6).fill(null);
|
||||
next[0] = currentActiveId;
|
||||
let slot = 1;
|
||||
for (const tab of currentTabs) {
|
||||
if (slot >= count) break;
|
||||
if (tab.id !== currentActiveId && tab.type !== "dashboard") {
|
||||
next[slot] = tab.id;
|
||||
slot++;
|
||||
}
|
||||
}
|
||||
setSplitMode(mode);
|
||||
setPaneTabIds(next);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Alt+Arrow — navigate between panes in split mode
|
||||
if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
|
||||
if (
|
||||
e.code === "ArrowLeft" ||
|
||||
e.code === "ArrowRight" ||
|
||||
e.code === "ArrowUp" ||
|
||||
e.code === "ArrowDown"
|
||||
) {
|
||||
if (splitModeRef.current === "none") return;
|
||||
const count = PANE_COUNTS[splitModeRef.current];
|
||||
if (count < 2) return;
|
||||
e.preventDefault();
|
||||
const current = focusedPaneIndexRef.current ?? 0;
|
||||
const mode = splitModeRef.current;
|
||||
const dir = e.code;
|
||||
|
||||
// Layout-aware navigation maps: [left, right, up, down] per pane index.
|
||||
// null means no movement in that direction.
|
||||
const navMap: Record<string, (number | null)[][]> = {
|
||||
"2-way": [
|
||||
[null, 1, null, null],
|
||||
[0, null, null, null],
|
||||
],
|
||||
"3-way": [
|
||||
[null, 1, null, null],
|
||||
[0, null, null, 2],
|
||||
[0, null, 1, null],
|
||||
],
|
||||
"3-way-horizontal": [
|
||||
[null, 1, null, 2],
|
||||
[0, null, null, 2],
|
||||
[null, null, 0, null],
|
||||
],
|
||||
"4-way": [
|
||||
[null, 1, null, 2],
|
||||
[0, null, null, 3],
|
||||
[null, 3, 0, null],
|
||||
[2, null, 1, null],
|
||||
],
|
||||
"5-way": [
|
||||
[null, 1, null, 3],
|
||||
[0, 2, null, 4],
|
||||
[1, null, null, 4],
|
||||
[null, 4, 0, null],
|
||||
[3, null, 1, null],
|
||||
],
|
||||
"6-way": [
|
||||
[null, 1, null, 3],
|
||||
[0, 2, null, 4],
|
||||
[1, null, null, 5],
|
||||
[null, 4, 0, null],
|
||||
[3, 5, 1, null],
|
||||
[4, null, 2, null],
|
||||
],
|
||||
};
|
||||
|
||||
const paneNav = navMap[mode]?.[current];
|
||||
const dirIndex =
|
||||
{ ArrowLeft: 0, ArrowRight: 1, ArrowUp: 2, ArrowDown: 3 }[dir] ??
|
||||
-1;
|
||||
const next = paneNav?.[dirIndex] ?? null;
|
||||
if (next === null) return;
|
||||
|
||||
focusedPaneIndexRef.current = next;
|
||||
setFocusedPaneIndex(next);
|
||||
// Physically move DOM focus into the target pane's terminal
|
||||
const tabId = paneTabIdsRef.current[next];
|
||||
if (tabId) {
|
||||
const termRef = terminalRefs.current.get(tabId);
|
||||
(
|
||||
termRef?.current as
|
||||
| import("@/features/terminal/Terminal").TerminalHandle
|
||||
| null
|
||||
)?.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+Shift+] / Ctrl+Shift+[ — cycle through open tabs (] = next, [ = previous)
|
||||
if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) {
|
||||
if (e.code === "BracketRight" || e.code === "BracketLeft") {
|
||||
e.preventDefault();
|
||||
const currentTabs = tabsRef.current;
|
||||
if (currentTabs.length < 2) return;
|
||||
const currentId = activeTabIdRef.current;
|
||||
const idx = currentTabs.findIndex((t) => t.id === currentId);
|
||||
const next =
|
||||
e.code === "BracketRight"
|
||||
? (idx + 1) % currentTabs.length
|
||||
: (idx - 1 + currentTabs.length) % currentTabs.length;
|
||||
setActiveTabId(currentTabs[next].id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
globalShortcutHandler.current = handleKeyDown;
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
globalShortcutHandler.current = null;
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const v = localStorage.getItem("commandPaletteShortcutEnabled");
|
||||
@@ -623,11 +822,17 @@ export function AppShell({
|
||||
const restoredSessionId =
|
||||
liveSession?.sessionId ?? saved.backendSessionId ?? null;
|
||||
|
||||
const isCustomLabel =
|
||||
host &&
|
||||
saved.label !== host.name &&
|
||||
!/^.+ \(\d+\)$/.test(saved.label);
|
||||
|
||||
restoredTabs.push({
|
||||
id: tabId,
|
||||
instanceId: saved.id,
|
||||
type: saved.tabType as TabType,
|
||||
label: saved.label,
|
||||
customLabel: isCustomLabel ? saved.label : undefined,
|
||||
host,
|
||||
openedAt: new Date(saved.createdAt).getTime(),
|
||||
restoredSessionId,
|
||||
@@ -694,7 +899,12 @@ export function AppShell({
|
||||
const openTab = useCallback(function openTab(
|
||||
host: Host,
|
||||
type: TabType,
|
||||
restore?: { instanceId: string; restoredSessionId: string | null },
|
||||
restore?: {
|
||||
instanceId: string;
|
||||
restoredSessionId: string | null;
|
||||
savedLabel?: string;
|
||||
initialFilePath?: string;
|
||||
},
|
||||
) {
|
||||
const tabId = `${host.name}-${type}-${Date.now()}`;
|
||||
const instanceId =
|
||||
@@ -707,7 +917,34 @@ export function AppShell({
|
||||
if (ref) terminalRefs.current.set(tabId, ref);
|
||||
|
||||
let finalLabel = host.name;
|
||||
const savedLabel = restore?.savedLabel;
|
||||
const initialFilePath = restore?.initialFilePath;
|
||||
// A saved label that doesn't match the bare host name or the auto-numbered pattern is a custom label
|
||||
const isCustomLabel =
|
||||
savedLabel != null &&
|
||||
savedLabel !== host.name &&
|
||||
!/^.+ \(\d+\)$/.test(savedLabel);
|
||||
|
||||
setTabs((prev) => {
|
||||
if (isCustomLabel && savedLabel) {
|
||||
finalLabel = savedLabel;
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: tabId,
|
||||
instanceId,
|
||||
type,
|
||||
label: finalLabel,
|
||||
customLabel: finalLabel,
|
||||
host,
|
||||
openedAt,
|
||||
terminalRef: ref,
|
||||
restoredSessionId: restore?.restoredSessionId ?? null,
|
||||
initialFilePath,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const same = prev.filter(
|
||||
(t) =>
|
||||
t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name,
|
||||
@@ -734,6 +971,7 @@ export function AppShell({
|
||||
openedAt,
|
||||
terminalRef: ref,
|
||||
restoredSessionId: restore?.restoredSessionId ?? null,
|
||||
initialFilePath,
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -789,6 +1027,9 @@ export function AppShell({
|
||||
),
|
||||
0,
|
||||
);
|
||||
} else if (pendingEvent === "host-manager:show-credentials") {
|
||||
setSidebarOpen(true);
|
||||
setRailView("credentials");
|
||||
} else {
|
||||
setSidebarOpen(true);
|
||||
setRailView("hosts");
|
||||
@@ -919,6 +1160,18 @@ export function AppShell({
|
||||
doCloseTab(id);
|
||||
}
|
||||
|
||||
function renameTab(tabId: string, newLabel: string) {
|
||||
setTabs((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === tabId ? { ...t, customLabel: newLabel, label: newLabel } : t,
|
||||
),
|
||||
);
|
||||
const tab = tabs.find((t) => t.id === tabId);
|
||||
if (tab?.instanceId) {
|
||||
patchOpenTab(tab.instanceId, { label: newLabel }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function splitTabQuick(tabId: string, mode: SplitMode) {
|
||||
setSplitMode(mode);
|
||||
setPaneTabIds(() => {
|
||||
@@ -1182,6 +1435,7 @@ export function AppShell({
|
||||
openTab(host, record.tabType as TabType, {
|
||||
instanceId: record.id,
|
||||
restoredSessionId: effectiveSessionId,
|
||||
savedLabel: record.label,
|
||||
});
|
||||
} else {
|
||||
openSingletonTab(record.tabType as TabType);
|
||||
@@ -1193,6 +1447,8 @@ export function AppShell({
|
||||
prev.filter((r) => r.id !== recordId),
|
||||
);
|
||||
}}
|
||||
onRenameTab={renameTab}
|
||||
onReorderTabs={setTabs}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -1208,6 +1464,7 @@ export function AppShell({
|
||||
<UserProfilePanel
|
||||
username={username}
|
||||
onLogout={onLogout}
|
||||
onChangeServer={onChangeServer}
|
||||
userPrefs={userPrefs}
|
||||
onPrefsChange={setUserPrefs}
|
||||
/>
|
||||
@@ -1264,8 +1521,6 @@ export function AppShell({
|
||||
splitMode={splitMode}
|
||||
username={username}
|
||||
isAdmin={isAdmin}
|
||||
profileDropdownOpen={profileDropdownOpen}
|
||||
onProfileDropdownChange={setProfileDropdownOpen}
|
||||
onRailClick={handleRailClick}
|
||||
onOpenTab={openSingletonTab}
|
||||
onLogout={onLogout}
|
||||
@@ -1334,6 +1589,7 @@ export function AppShell({
|
||||
onSplitTab={splitTabQuick}
|
||||
onAddToSplit={addTabToSplit}
|
||||
onRemoveFromSplit={removeTabFromSplit}
|
||||
onRenameTab={renameTab}
|
||||
/>
|
||||
<div className="relative flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
{/* Split view — always mounted when not mobile, hidden via CSS when inactive */}
|
||||
@@ -1388,6 +1644,17 @@ export function AppShell({
|
||||
openTab,
|
||||
closeTab,
|
||||
inPane || activeInline,
|
||||
(host, filePath) =>
|
||||
openTab(host, "files", {
|
||||
instanceId:
|
||||
typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`,
|
||||
restoredSessionId: null,
|
||||
initialFilePath: filePath,
|
||||
}),
|
||||
(host, path) => openTab(host, "files"),
|
||||
renameTab,
|
||||
),
|
||||
tabNode,
|
||||
tab.id,
|
||||
|
||||
Reference in New Issue
Block a user