* Add documentation in Chinese language (#160)

* Update file naming and structure for mobile support

* Add conditional desktop/mobile rendering

* Mobile terminal

* Fix overwritten i18n (#161)

* Add comprehensive Chinese internationalization support

- Implemented i18n framework with react-i18next for multi-language support
- Added Chinese (zh) and English (en) translation files with comprehensive coverage
- Localized Admin interface, authentication flows, and error messages
- Translated FileManager operations and UI elements
- Updated HomepageAuth component with localized authentication messages
- Localized LeftSidebar navigation and host management
- Added language switcher component (shown after login only)
- Configured default language as English with Chinese as secondary option
- Localized TOTPSetup two-factor authentication interface
- Updated Docker build to include translation files
- Achieved 95%+ UI localization coverage across core components

Co-Authored-By: Claude <noreply@anthropic.com>

* Extend Chinese localization coverage to Host Manager components

- Added comprehensive translations for HostManagerHostViewer component
- Localized all host management UI text including import/export features
- Translated error messages and confirmation dialogs for host operations
- Added translations for HostManagerHostEditor validation messages
- Localized connection details, organization settings, and form labels
- Fixed syntax error in FileManagerOperations component
- Achieved near-complete localization of SSH host management interface
- Updated placeholders and tooltips for better user guidance

Co-Authored-By: Claude <noreply@anthropic.com>

* Complete comprehensive Chinese localization for Termix

- Added full localization support for Tunnel components (connected/disconnected states, retry messages)
- Localized all tunnel status messages and connection errors
- Added translations for port forwarding UI elements
- Verified Server, TopNavbar, and Tab components already have complete i18n support
- Achieved 99%+ localization coverage across entire application
- All core UI components now fully support Chinese and English languages

This completes the comprehensive internationalization effort for the Termix SSH management platform.

Co-Authored-By: Claude <noreply@anthropic.com>

* Localize additional Host Manager components and authentication settings

- Added translations for all authentication options (Password, Key, SSH Private Key)
- Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager)
- Translated Upload/Update Key button states
- Localized Host Viewer and Add/Edit Host tab labels
- Added Chinese translations for all host management settings
- Fixed duplicate translation keys in JSON files

Co-Authored-By: Claude <noreply@anthropic.com>

* Extend localization coverage to UI components and common strings

- Added comprehensive common translations (online/offline, success/error, etc.)
- Localized status indicator component with all status states
- Updated FileManagerLeftSidebar toast messages for rename/delete operations
- Added translations for UI elements (close, toggle sidebar, etc.)
- Expanded placeholder translations for form inputs
- Added Chinese translations for all new common strings
- Improved consistency across component status messages

Co-Authored-By: Claude <noreply@anthropic.com>

* Complete Chinese localization for remaining UI components

- Add comprehensive Chinese translations for Host Manager component
  - Translate all form labels, buttons, and descriptions
  - Add translations for SSH configuration warnings and instructions
  - Localize tunnel connection settings and port forwarding options

- Localize SSH Tools panel
  - Translate key recording functionality
  - Add translations for settings and configuration options

- Translate homepage welcome messages and navigation elements
  - Add Chinese translations for login success messages
  - Localize "Updates & Releases" section title
  - Translate sidebar "Host Manager" button

- Fix translation key display issues
  - Remove duplicate translation keys in both language files
  - Ensure all components properly reference translation keys
  - Fix hosts.tunnelConnections key mapping

This completes the full Chinese localization of the Termix application,
achieving near 100% UI translation coverage while maintaining English
as the default language.

* Complete final Chinese localization for Host Manager tunnel configuration

- Add Chinese translations for authentication UI elements
  - Translate "Authentication", "Password", and "Key" tab labels
  - Localize SSH private key and key password fields
  - Add translations for key type selector

- Localize tunnel connection configuration descriptions
  - Translate retry attempts and retry interval descriptions
  - Add dynamic tunnel forwarding description with port parameters
  - Localize endpoint SSH configuration labels

- Fix missing translation keys
  - Add "upload" translation for file upload button
  - Ensure all FormLabel and FormDescription elements use translation keys

This completes the comprehensive Chinese localization of the entire
Termix application, achieving 100% UI translation coverage.

* Fix PR feedback: Improve Profile section translations and UX

- Fixed password reset translations in Profile section
- Moved language selector from TopNavbar to Profile page
- Added profile.selectPreferredLanguage translation key
- Improved user experience for language preferences

* Apply critical OIDC and notification system fixes while preserving i18n

- Merge OIDC authentication fixes from 3877e90:
  * Enhanced JWKS discovery mechanism with multiple backup URLs
  * Better support for non-standard OIDC providers (Authentik, etc.)
  * Improved error handling for "Failed to get user information"
- Migrate to unified Sonner toast notification system:
  * Replace custom success/error state management
  * Remove redundant alert state variables
  * Consistent user feedback across all components
- Improve code quality and function naming conventions
- PRESERVE all existing i18n functionality and Chinese translations

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix OIDC errors for "Failed to get user information"

* Fix OIDC errors for "Failed to get user information"

* Fix spelling error

* Migrate everything to alert system, update user.ts for OIDC updates.

* Fix OIDC errors for "Failed to get user information"

* Fix OIDC errors for "Failed to get user information"

* Fix spelling error

* Migrate everything to alert system, update user.ts for OIDC updates.

* Update env

* Fix users.ts and schema for override

* Convert web app to Electron desktop application

- Add Electron main process with developer tools support
- Create preload script for secure context bridge
- Configure electron-builder for packaging
- Update Vite config for Electron compatibility (base: './')
- Add environment variable support for API host configuration
- Fix i18n to use relative paths for Electron file protocol
- Restore multi-port backend architecture (8081-8085)
- Add enhanced backend startup script with port checking
- Update package.json with Electron dependencies and build scripts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Complete Electron desktop application implementation

- Add backend auto-start functionality in main process
- Fix authentication token storage for Electron environment
- Implement localStorage-based token management in Electron
- Add proper Electron environment detection via preload script
- Fix WebSocket connections for terminal functionality
- Resolve font file loading issues in packaged application
- Update API endpoints to work with backend auto-start
- Streamline build scripts with unified electron:package command
- Fix better-sqlite3 native module compatibility issues
- Ensure all services start automatically in production mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove releases folder from git and force Desktop UI.

* Improve mobile support with half-baked custom keyboard

* Fix API routing

* Upgrade mobile keyboard with more keys.

* Add cross-platform support and clean up obsolete files

- Add electron-packager scripts for Windows, macOS, and Linux
- Include universal architecture support for macOS
- Add electron:package:all for building all platforms
- Remove obsolete start-backend.sh script (replaced by Electron auto-start)
- Improve ignore patterns to exclude repo-images folder
- Add platform-specific icon configurations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix build system by removing electron-builder dependency

- Remove electron-builder and @electron/rebuild packages to resolve build errors
- Clean up package.json scripts that depend on electron-builder
- Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx
- All build commands now work correctly:
  - npm run build (frontend + backend)
  - npm run build:frontend
  - npm run build:backend
  - npm run electron:package (using electron-packager)

The build system is now stable and functional without signing requirements.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* Mobile UI improvement

* Electron dev (#185)

* Add comprehensive Chinese internationalization support

- Implemented i18n framework with react-i18next for multi-language support
- Added Chinese (zh) and English (en) translation files with comprehensive coverage
- Localized Admin interface, authentication flows, and error messages
- Translated FileManager operations and UI elements
- Updated HomepageAuth component with localized authentication messages
- Localized LeftSidebar navigation and host management
- Added language switcher component (shown after login only)
- Configured default language as English with Chinese as secondary option
- Localized TOTPSetup two-factor authentication interface
- Updated Docker build to include translation files
- Achieved 95%+ UI localization coverage across core components

Co-Authored-By: Claude <noreply@anthropic.com>

* Extend Chinese localization coverage to Host Manager components

- Added comprehensive translations for HostManagerHostViewer component
- Localized all host management UI text including import/export features
- Translated error messages and confirmation dialogs for host operations
- Added translations for HostManagerHostEditor validation messages
- Localized connection details, organization settings, and form labels
- Fixed syntax error in FileManagerOperations component
- Achieved near-complete localization of SSH host management interface
- Updated placeholders and tooltips for better user guidance

Co-Authored-By: Claude <noreply@anthropic.com>

* Complete comprehensive Chinese localization for Termix

- Added full localization support for Tunnel components (connected/disconnected states, retry messages)
- Localized all tunnel status messages and connection errors
- Added translations for port forwarding UI elements
- Verified Server, TopNavbar, and Tab components already have complete i18n support
- Achieved 99%+ localization coverage across entire application
- All core UI components now fully support Chinese and English languages

This completes the comprehensive internationalization effort for the Termix SSH management platform.

Co-Authored-By: Claude <noreply@anthropic.com>

* Localize additional Host Manager components and authentication settings

- Added translations for all authentication options (Password, Key, SSH Private Key)
- Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager)
- Translated Upload/Update Key button states
- Localized Host Viewer and Add/Edit Host tab labels
- Added Chinese translations for all host management settings
- Fixed duplicate translation keys in JSON files

Co-Authored-By: Claude <noreply@anthropic.com>

* Extend localization coverage to UI components and common strings

- Added comprehensive common translations (online/offline, success/error, etc.)
- Localized status indicator component with all status states
- Updated FileManagerLeftSidebar toast messages for rename/delete operations
- Added translations for UI elements (close, toggle sidebar, etc.)
- Expanded placeholder translations for form inputs
- Added Chinese translations for all new common strings
- Improved consistency across component status messages

Co-Authored-By: Claude <noreply@anthropic.com>

* Complete Chinese localization for remaining UI components

- Add comprehensive Chinese translations for Host Manager component
  - Translate all form labels, buttons, and descriptions
  - Add translations for SSH configuration warnings and instructions
  - Localize tunnel connection settings and port forwarding options

- Localize SSH Tools panel
  - Translate key recording functionality
  - Add translations for settings and configuration options

- Translate homepage welcome messages and navigation elements
  - Add Chinese translations for login success messages
  - Localize "Updates & Releases" section title
  - Translate sidebar "Host Manager" button

- Fix translation key display issues
  - Remove duplicate translation keys in both language files
  - Ensure all components properly reference translation keys
  - Fix hosts.tunnelConnections key mapping

This completes the full Chinese localization of the Termix application,
achieving near 100% UI translation coverage while maintaining English
as the default language.

* Complete final Chinese localization for Host Manager tunnel configuration

- Add Chinese translations for authentication UI elements
  - Translate "Authentication", "Password", and "Key" tab labels
  - Localize SSH private key and key password fields
  - Add translations for key type selector

- Localize tunnel connection configuration descriptions
  - Translate retry attempts and retry interval descriptions
  - Add dynamic tunnel forwarding description with port parameters
  - Localize endpoint SSH configuration labels

- Fix missing translation keys
  - Add "upload" translation for file upload button
  - Ensure all FormLabel and FormDescription elements use translation keys

This completes the comprehensive Chinese localization of the entire
Termix application, achieving 100% UI translation coverage.

* Fix PR feedback: Improve Profile section translations and UX

- Fixed password reset translations in Profile section
- Moved language selector from TopNavbar to Profile page
- Added profile.selectPreferredLanguage translation key
- Improved user experience for language preferences

* Apply critical OIDC and notification system fixes while preserving i18n

- Merge OIDC authentication fixes from 3877e90:
  * Enhanced JWKS discovery mechanism with multiple backup URLs
  * Better support for non-standard OIDC providers (Authentik, etc.)
  * Improved error handling for "Failed to get user information"
- Migrate to unified Sonner toast notification system:
  * Replace custom success/error state management
  * Remove redundant alert state variables
  * Consistent user feedback across all components
- Improve code quality and function naming conventions
- PRESERVE all existing i18n functionality and Chinese translations

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix OIDC errors for "Failed to get user information"

* Fix OIDC errors for "Failed to get user information"

* Fix spelling error

* Migrate everything to alert system, update user.ts for OIDC updates.

* Fix OIDC errors for "Failed to get user information"

* Fix OIDC errors for "Failed to get user information"

* Fix spelling error

* Migrate everything to alert system, update user.ts for OIDC updates.

* Update env

* Fix users.ts and schema for override

* Convert web app to Electron desktop application

- Add Electron main process with developer tools support
- Create preload script for secure context bridge
- Configure electron-builder for packaging
- Update Vite config for Electron compatibility (base: './')
- Add environment variable support for API host configuration
- Fix i18n to use relative paths for Electron file protocol
- Restore multi-port backend architecture (8081-8085)
- Add enhanced backend startup script with port checking
- Update package.json with Electron dependencies and build scripts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Complete Electron desktop application implementation

- Add backend auto-start functionality in main process
- Fix authentication token storage for Electron environment
- Implement localStorage-based token management in Electron
- Add proper Electron environment detection via preload script
- Fix WebSocket connections for terminal functionality
- Resolve font file loading issues in packaged application
- Update API endpoints to work with backend auto-start
- Streamline build scripts with unified electron:package command
- Fix better-sqlite3 native module compatibility issues
- Ensure all services start automatically in production mode

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove releases folder from git and force Desktop UI.

* Improve mobile support with half-baked custom keyboard

* Fix API routing

* Upgrade mobile keyboard with more keys.

* Add cross-platform support and clean up obsolete files

- Add electron-packager scripts for Windows, macOS, and Linux
- Include universal architecture support for macOS
- Add electron:package:all for building all platforms
- Remove obsolete start-backend.sh script (replaced by Electron auto-start)
- Improve ignore patterns to exclude repo-images folder
- Add platform-specific icon configurations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix build system by removing electron-builder dependency

- Remove electron-builder and @electron/rebuild packages to resolve build errors
- Clean up package.json scripts that depend on electron-builder
- Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx
- All build commands now work correctly:
  - npm run build (frontend + backend)
  - npm run build:frontend
  - npm run build:backend
  - npm run electron:package (using electron-packager)

The build system is now stable and functional without signing requirements.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Karmaa <88517757+LukeGus@users.noreply.github.com>

* Add navigation and hardcoded hosts

* Update mobile sidebar to use API, add auth and tab system to mobile.

* Update sidebar state

* Mobile support (#190)

* Add vibration to keyboard

* Fix keyboard keys

* Fix keyboard keys

* Fix keyboard keys

* Rename files, improve keyboard usability

* Improve keyboard view and fix various issues with it

* Add mobile chinese translation

* Disable OS keyboard from appearing

* Fix fit addon not resizing with "more" on keyboard

* Disable OS keyboard on terminal load

* Merge Luke and Zac

* feat: add export  option for ssh hosts (#173) (#187)

* Update issue templates

* feat: add export JSON option for SSH hosts (#173)

---------

Co-authored-by: Karmaa <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* feat(profile): display version number from .env in profile menu (#182)

* feat(profile): display version number from .env in profile menu

* Update version checking process

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* Add pretier

* feat(auth): Add password visibility toggle to auth forms (#166)

* added hide and unhide password button

* Undo admin settings changes

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* Re-added password input

* Remove encrpytion, improve logging and merge interfaces.

* Improve logging (backend and frontend) and added dedicde OIDC clear

* feat: Added option to paste private key (#203)

* Improve logging frontend/backend, fix host form being reversed.

* Improve logging more, fix credentials sync issues, migrate more to be toasts

* Improve logging more, fix credentials sync issues, migrate more to be toasts

* More error to toast migration

* Remove more inline styles and run npm updates

* Update homepage appearing over everything and terminal incorrect bg

* Improved server stat generation and UI by caching and supporting more platforms

* Update mobile app with the same stat changes and remove rate limiting

* Put user profle in its own tab, add code rabbit support

* Improve code rabbit yaml

* Update chinese translation and fix z indexs causing delay to hide

* Bump vite from 7.1.3 to 7.1.5 (#204)

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.3 to 7.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update read me

* Update electron builder and fix mobile terminal background

* Update logo, move translations, update electron building.

* Remove backend from electon, switching to server manager

* Add electron server configurator

* Fix backend builder on Dockerfile

* Fix langauge file for Dockerfile

* Fix architecture issues in Dockerfile

* Fix architecture issues in Dockerfile

* Fix architecture issues in Dockerfile

* Fix backend building for docker image

* Add electron builder

* Fix node starting in entrypoint and remove release from electron build

* Remove double packaing in electron build

* Fix folder nesting for electron gbuilder

* Fix native module docker build (better-sql and bcrypt)

* Fix api routes and missing translations and improve reconnection for terminals

* Update read me for new installation method

* Update CONTRIBUTING.md with color scheme

* Fix terrminal not closing afer 3 tries

* Fix electronm api routing, fikx ssh not connecting, and OIDC redirect errors

* Fix more electron API issues (ssh/oidc), make server manager force API check, and login saving.

* Add electron API routes

* Fix more electron APi routes and issues

* Hide admin settings on electron and fix server manager URl verification

* Hide admin settings on electron and fix server manager URl verification

* Fix admin setting visiblity on electron

* Add links to docs in respective places

* Migrate all getCookies to use main-axios.

* Migrate all isElectron to use main-axios.

* Clean up backend files

* Clean up frontend files and read me translations

* Run prettier

* Fix terminal in web, and update translations and prep for release.

* Update API to work on devs and remove random letter

* Run prettier

* Update read me for release

* Update read me for release

* Fixed delete issue (ready for release)

* Ensure retention days for artifact upload are set

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: starry <115192496+sky22333@users.noreply.github.com>
Co-authored-by: ZacharyZcR <PayasoNorahC@protonmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com>
Co-authored-by: Abhilash Gandhamalla <150357125+AbhilashG12@users.noreply.github.com>
Co-authored-by: jedi04 <78037206+jedi04@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit was merged in pull request #221.
This commit is contained in:
Karmaa
2025-09-12 14:42:00 -05:00
committed by GitHub
parent d85fb26a5d
commit 5cd9de9ac5
187 changed files with 42370 additions and 20790 deletions

View File

@@ -0,0 +1,634 @@
import {
useEffect,
useRef,
useState,
useImperativeHandle,
forwardRef,
} from "react";
import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { Unicode11Addon } from "@xterm/addon-unicode11";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { getCookie, isElectron } from "@/ui/main-axios.ts";
interface SSHTerminalProps {
hostConfig: any;
isVisible: boolean;
title?: string;
showTitle?: boolean;
splitScreen?: boolean;
onClose?: () => void;
}
export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
{ hostConfig, isVisible, splitScreen = false, onClose },
ref,
) {
const { t } = useTranslation();
const { instance: terminal, ref: xtermRef } = useXTerm();
const fitAddonRef = useRef<FitAddon | null>(null);
const webSocketRef = useRef<WebSocket | null>(null);
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
const wasDisconnectedBySSH = useRef(false);
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const [visible, setVisible] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null);
const isVisibleRef = useRef<boolean>(false);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const reconnectAttempts = useRef(0);
const maxReconnectAttempts = 3;
const isUnmountingRef = useRef(false);
const shouldNotReconnectRef = useRef(false);
const isReconnectingRef = useRef(false);
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
const DEBOUNCE_MS = 140;
useEffect(() => {
isVisibleRef.current = isVisible;
}, [isVisible]);
function hardRefresh() {
try {
if (terminal && typeof (terminal as any).refresh === "function") {
(terminal as any).refresh(0, terminal.rows - 1);
}
} catch (_) {}
}
function scheduleNotify(cols: number, rows: number) {
if (!(cols > 0 && rows > 0)) return;
pendingSizeRef.current = { cols, rows };
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
notifyTimerRef.current = setTimeout(() => {
const next = pendingSizeRef.current;
const last = lastSentSizeRef.current;
if (!next) return;
if (last && last.cols === next.cols && last.rows === next.rows) return;
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
webSocketRef.current.send(
JSON.stringify({ type: "resize", data: next }),
);
lastSentSizeRef.current = next;
}
}, DEBOUNCE_MS);
}
useImperativeHandle(
ref,
() => ({
disconnect: () => {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
webSocketRef.current?.close();
setIsConnected(false);
setIsConnecting(false);
},
fit: () => {
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
},
sendInput: (data: string) => {
if (webSocketRef.current?.readyState === 1) {
webSocketRef.current.send(JSON.stringify({ type: "input", data }));
}
},
notifyResize: () => {
try {
const cols = terminal?.cols ?? undefined;
const rows = terminal?.rows ?? undefined;
if (typeof cols === "number" && typeof rows === "number") {
scheduleNotify(cols, rows);
hardRefresh();
}
} catch (_) {}
},
refresh: () => hardRefresh(),
}),
[terminal],
);
useEffect(() => {
window.addEventListener("resize", handleWindowResize);
return () => window.removeEventListener("resize", handleWindowResize);
}, []);
function handleWindowResize() {
if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
}
function getUseRightClickCopyPaste() {
return getCookie("rightClickCopyPaste") === "true";
}
function attemptReconnection() {
if (
isUnmountingRef.current ||
shouldNotReconnectRef.current ||
isReconnectingRef.current
) {
return;
}
if (reconnectAttempts.current >= maxReconnectAttempts) {
toast.error(t("terminal.maxReconnectAttemptsReached"));
if (onClose) {
onClose();
}
return;
}
isReconnectingRef.current = true;
if (terminal) {
terminal.clear();
}
reconnectAttempts.current++;
toast.info(
t("terminal.reconnecting", {
attempt: reconnectAttempts.current,
max: maxReconnectAttempts,
}),
);
reconnectTimeoutRef.current = setTimeout(() => {
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
isReconnectingRef.current = false;
return;
}
if (reconnectAttempts.current > maxReconnectAttempts) {
isReconnectingRef.current = false;
return;
}
if (terminal && hostConfig) {
terminal.clear();
const cols = terminal.cols;
const rows = terminal.rows;
connectToHost(cols, rows);
}
isReconnectingRef.current = false;
}, 2000 * reconnectAttempts.current);
}
function connectToHost(cols: number, rows: number) {
const isDev =
process.env.NODE_ENV === "development" &&
(window.location.port === "3000" ||
window.location.port === "5173" ||
window.location.port === "");
const wsUrl = isDev
? "ws://localhost:8082"
: isElectron()
? (() => {
const baseUrl =
(window as any).configuredServerUrl || "http://127.0.0.1:8081";
const wsProtocol = baseUrl.startsWith("https://")
? "wss://"
: "ws://";
const wsHost = baseUrl.replace(/^https?:\/\//, "");
return `${wsProtocol}${wsHost}/ssh/websocket/`;
})()
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
const ws = new WebSocket(wsUrl);
webSocketRef.current = ws;
wasDisconnectedBySSH.current = false;
setConnectionError(null);
shouldNotReconnectRef.current = false;
isReconnectingRef.current = false;
setIsConnecting(true);
setupWebSocketListeners(ws, cols, rows);
}
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
ws.addEventListener("open", () => {
connectionTimeoutRef.current = setTimeout(() => {
if (!isConnected) {
if (terminal) {
terminal.clear();
}
toast.error(t("terminal.connectionTimeout"));
if (webSocketRef.current) {
webSocketRef.current.close();
}
if (reconnectAttempts.current > 0) {
attemptReconnection();
}
}
}, 10000);
ws.send(
JSON.stringify({
type: "connectToHost",
data: { cols, rows, hostConfig },
}),
);
terminal.onData((data) => {
ws.send(JSON.stringify({ type: "input", data }));
});
pingIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }));
}
}, 30000);
});
ws.addEventListener("message", (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "data") {
terminal.write(msg.data);
} else if (msg.type === "error") {
const errorMessage = msg.message || t("terminal.unknownError");
if (
errorMessage.toLowerCase().includes("auth") ||
errorMessage.toLowerCase().includes("password") ||
errorMessage.toLowerCase().includes("permission") ||
errorMessage.toLowerCase().includes("denied") ||
errorMessage.toLowerCase().includes("invalid") ||
errorMessage.toLowerCase().includes("failed") ||
errorMessage.toLowerCase().includes("incorrect")
) {
toast.error(t("terminal.authError", { message: errorMessage }));
shouldNotReconnectRef.current = true;
if (webSocketRef.current) {
webSocketRef.current.close();
}
if (onClose) {
onClose();
}
return;
}
if (
errorMessage.toLowerCase().includes("connection") ||
errorMessage.toLowerCase().includes("timeout") ||
errorMessage.toLowerCase().includes("network")
) {
toast.error(
t("terminal.connectionError", { message: errorMessage }),
);
setIsConnected(false);
if (terminal) {
terminal.clear();
}
setIsConnecting(true);
attemptReconnection();
return;
}
toast.error(t("terminal.error", { message: errorMessage }));
} else if (msg.type === "connected") {
setIsConnected(true);
setIsConnecting(false);
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
if (reconnectAttempts.current > 0) {
toast.success(t("terminal.reconnected"));
}
reconnectAttempts.current = 0;
isReconnectingRef.current = false;
} else if (msg.type === "disconnected") {
wasDisconnectedBySSH.current = true;
setIsConnected(false);
if (terminal) {
terminal.clear();
}
setIsConnecting(true);
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
attemptReconnection();
}
}
} catch (error) {
toast.error(t("terminal.messageParseError"));
}
});
ws.addEventListener("close", (event) => {
setIsConnected(false);
if (terminal) {
terminal.clear();
}
setIsConnecting(true);
if (
!wasDisconnectedBySSH.current &&
!isUnmountingRef.current &&
!shouldNotReconnectRef.current
) {
attemptReconnection();
}
});
ws.addEventListener("error", (event) => {
setIsConnected(false);
setConnectionError(t("terminal.websocketError"));
if (terminal) {
terminal.clear();
}
setIsConnecting(true);
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
attemptReconnection();
}
});
}
async function writeTextToClipboard(text: string): Promise<void> {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
} catch (_) {}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
} finally {
document.body.removeChild(textarea);
}
}
async function readTextFromClipboard(): Promise<string> {
try {
if (navigator.clipboard && navigator.clipboard.readText) {
return await navigator.clipboard.readText();
}
} catch (_) {}
return "";
}
useEffect(() => {
if (!terminal || !xtermRef.current || !hostConfig) return;
terminal.options = {
cursorBlink: true,
cursorStyle: "bar",
scrollback: 10000,
fontSize: 14,
fontFamily:
'"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
theme: { background: "#18181b", foreground: "#f7f7f7" },
allowTransparency: true,
convertEol: true,
windowsMode: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
rightClickSelectsWord: false,
fastScrollModifier: "alt",
fastScrollSensitivity: 5,
allowProposedApi: true,
};
const fitAddon = new FitAddon();
const clipboardAddon = new ClipboardAddon();
const unicode11Addon = new Unicode11Addon();
const webLinksAddon = new WebLinksAddon();
fitAddonRef.current = fitAddon;
terminal.loadAddon(fitAddon);
terminal.loadAddon(clipboardAddon);
terminal.loadAddon(unicode11Addon);
terminal.loadAddon(webLinksAddon);
terminal.open(xtermRef.current);
const element = xtermRef.current;
const handleContextMenu = async (e: MouseEvent) => {
if (!getUseRightClickCopyPaste()) return;
e.preventDefault();
e.stopPropagation();
try {
if (terminal.hasSelection()) {
const selection = terminal.getSelection();
if (selection) {
await writeTextToClipboard(selection);
terminal.clearSelection();
}
} else {
const pasteText = await readTextFromClipboard();
if (pasteText) terminal.paste(pasteText);
}
} catch (_) {}
};
element?.addEventListener("contextmenu", handleContextMenu);
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(() => {
if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
}, 100);
});
resizeObserver.observe(xtermRef.current);
const readyFonts =
(document as any).fonts?.ready instanceof Promise
? (document as any).fonts.ready
: Promise.resolve();
readyFonts.then(() => {
setTimeout(() => {
fitAddon.fit();
setTimeout(() => {
fitAddon.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
setVisible(true);
if (terminal && !splitScreen) {
terminal.focus();
}
}, 0);
const cols = terminal.cols;
const rows = terminal.rows;
connectToHost(cols, rows);
}, 300);
});
return () => {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
setIsConnecting(false);
resizeObserver.disconnect();
element?.removeEventListener("contextmenu", handleContextMenu);
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
if (reconnectTimeoutRef.current)
clearTimeout(reconnectTimeoutRef.current);
if (connectionTimeoutRef.current)
clearTimeout(connectionTimeoutRef.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
webSocketRef.current?.close();
};
}, [xtermRef, terminal, hostConfig]);
useEffect(() => {
if (isVisible && fitAddonRef.current) {
setTimeout(() => {
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
if (terminal && !splitScreen) {
terminal.focus();
}
}, 0);
if (terminal && !splitScreen) {
setTimeout(() => {
terminal.focus();
}, 100);
}
}
}, [isVisible, splitScreen, terminal]);
useEffect(() => {
if (!fitAddonRef.current) return;
setTimeout(() => {
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
if (terminal && !splitScreen && isVisible) {
terminal.focus();
}
}, 0);
}, [splitScreen, isVisible, terminal]);
return (
<div className="h-full w-full m-1 relative">
{/* Terminal */}
<div
ref={xtermRef}
className={`h-full w-full transition-opacity duration-200 ${visible && isVisible && !isConnecting ? "opacity-100" : "opacity-0"} overflow-hidden`}
onClick={() => {
if (terminal && !splitScreen) {
terminal.focus();
}
}}
/>
{/* Connecting State */}
{isConnecting && (
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
<div className="flex items-center gap-3">
<div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
<span className="text-gray-300">{t("terminal.connecting")}</span>
</div>
</div>
)}
</div>
);
});
const style = document.createElement("style");
style.innerHTML = `
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
/* Load NerdFonts locally */
@font-face {
font-family: 'JetBrains Mono Nerd Font';
src: url('./fonts/JetBrainsMonoNerdFont-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono Nerd Font';
src: url('./fonts/JetBrainsMonoNerdFont-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono Nerd Font';
src: url('./fonts/JetBrainsMonoNerdFont-Italic.ttf') format('truetype');
font-weight: normal;
font-style: italic;
font-display: swap;
}
.xterm .xterm-viewport::-webkit-scrollbar {
width: 8px;
background: transparent;
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb {
background: rgba(180,180,180,0.7);
border-radius: 4px;
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
background: rgba(120,120,120,0.9);
}
.xterm .xterm-viewport {
scrollbar-width: thin;
scrollbar-color: rgba(180,180,180,0.7) transparent;
}
.xterm {
font-feature-settings: "liga" 1, "calt" 1;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.xterm .xterm-screen {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font', 'Cascadia Code', 'JetBrains Mono', Consolas, "Courier New", monospace !important;
font-variant-ligatures: contextual;
}
.xterm .xterm-screen .xterm-char {
font-feature-settings: "liga" 1, "calt" 1;
}
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE"] {
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
}
`;
document.head.appendChild(style);