* Feature request network graph

* Fixing PR442:

- Fixed:
    - UI design elemets
    - UI and button colors
    - JSON export
    - recent activity is default again
- Removed:
    - Online/Offline UI labels
    - left-click menu on hosts
- Added:
    - small pulsing dot inside the hosts to indicate online status like in the left bar

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

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

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

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

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

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

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

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

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

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

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

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

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

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

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

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

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

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>
This commit is contained in:
Luke Gustafson
2026-01-24 19:49:42 -06:00
committed by GitHub
parent 366667ca92
commit 536dc35258
169 changed files with 79905 additions and 47362 deletions
@@ -12,7 +12,6 @@ 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,
@@ -21,8 +20,9 @@ import {
deleteCommandFromHistory,
getCommandHistory,
} from "@/ui/main-axios.ts";
import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx";
import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx";
import { TOTPDialog } from "@/ui/desktop/navigation/dialogs/TOTPDialog.tsx";
import { SSHAuthDialog } from "@/ui/desktop/navigation/dialogs/SSHAuthDialog.tsx";
import { WarpgateDialog } from "@/ui/desktop/navigation/dialogs/WarpgateDialog.tsx";
import {
TERMINAL_THEMES,
DEFAULT_TERMINAL_CONFIG,
@@ -37,6 +37,11 @@ import { useCommandHistory } from "@/ui/desktop/apps/features/terminal/command-h
import { CommandAutocomplete } from "./command-history/CommandAutocomplete.tsx";
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
import { useConfirmation } from "@/hooks/use-confirmation.ts";
import {
ConnectionLogProvider,
useConnectionLog,
} from "@/ui/desktop/navigation/connection-log/ConnectionLogContext.tsx";
import { ConnectionLog } from "@/ui/desktop/navigation/connection-log/ConnectionLog.tsx";
interface HostConfig {
id?: number;
@@ -72,7 +77,7 @@ interface SSHTerminalProps {
executeCommand?: string;
}
export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
function SSHTerminal(
{
hostConfig,
@@ -99,6 +104,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const commandHistoryContext = useCommandHistory();
const { confirmWithToast } = useConfirmation();
const { theme: appTheme } = useTheme();
const { addLog, isExpanded: isConnectionLogExpanded } = useConnectionLog();
const config = { ...DEFAULT_TERMINAL_CONFIG, ...hostConfig.terminalConfig };
@@ -126,7 +132,14 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [isFitted, setIsFitted] = useState(false);
const [, setConnectionError] = useState<string | null>(null);
const [connectionError, setConnectionError] = useState<string | null>(null);
const connectionErrorRef = useRef<string | null>(null);
const updateConnectionError = useCallback((error: string | null) => {
connectionErrorRef.current = error;
setConnectionError(error);
}, []);
const [, setIsAuthenticated] = useState(false);
const [totpRequired, setTotpRequired] = useState(false);
const [totpPrompt, setTotpPrompt] = useState<string>("");
@@ -137,6 +150,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
>("no_keyboard");
const [keyboardInteractiveDetected, setKeyboardInteractiveDetected] =
useState(false);
const [warpgateAuthRequired, setWarpgateAuthRequired] = useState(false);
const [warpgateAuthUrl, setWarpgateAuthUrl] = useState<string>("");
const [warpgateSecurityKey, setWarpgateSecurityKey] = useState<string>("");
const warpgateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isVisibleRef = useRef<boolean>(false);
const isFittingRef = useRef(false);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -146,8 +163,17 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const shouldNotReconnectRef = useRef(false);
const isReconnectingRef = useRef(false);
const isConnectingRef = useRef(false);
useEffect(() => {
isUnmountingRef.current = false;
shouldNotReconnectRef.current = false;
isReconnectingRef.current = false;
isConnectingRef.current = false;
reconnectAttempts.current = 0;
return () => {};
}, [hostConfig.id]);
const connectionAttemptIdRef = useRef(0);
const totpAttemptsRef = useRef(0);
const totpTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const activityLoggedRef = useRef(false);
@@ -401,6 +427,41 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
if (onClose) onClose();
}
function handleWarpgateContinue() {
if (webSocketRef.current) {
if (warpgateTimeoutRef.current) {
clearTimeout(warpgateTimeoutRef.current);
warpgateTimeoutRef.current = null;
}
webSocketRef.current.send(
JSON.stringify({
type: "warpgate_auth_continue",
data: {},
}),
);
setWarpgateAuthRequired(false);
setWarpgateAuthUrl("");
setWarpgateSecurityKey("");
}
}
function handleWarpgateCancel() {
if (warpgateTimeoutRef.current) {
clearTimeout(warpgateTimeoutRef.current);
warpgateTimeoutRef.current = null;
}
setWarpgateAuthRequired(false);
setWarpgateAuthUrl("");
setWarpgateSecurityKey("");
if (onClose) onClose();
}
function handleWarpgateOpenUrl() {
if (warpgateAuthUrl) {
window.open(warpgateAuthUrl, "_blank", "noopener,noreferrer");
}
}
function handleAuthDialogSubmit(credentials: {
password?: string;
sshKey?: string;
@@ -476,6 +537,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
clearTimeout(totpTimeoutRef.current);
totpTimeoutRef.current = null;
}
if (warpgateTimeoutRef.current) {
clearTimeout(warpgateTimeoutRef.current);
warpgateTimeoutRef.current = null;
}
webSocketRef.current?.close();
setIsConnected(false);
setIsConnecting(false);
@@ -517,16 +582,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
shouldNotReconnectRef.current ||
isReconnectingRef.current ||
isConnectingRef.current ||
wasDisconnectedBySSH.current
wasDisconnectedBySSH.current ||
reconnectTimeoutRef.current !== null
) {
return;
}
if (reconnectAttempts.current >= maxReconnectAttempts) {
toast.error(t("terminal.maxReconnectAttemptsReached"));
if (onClose) {
onClose();
}
updateConnectionError(t("terminal.maxReconnectAttemptsReached"));
setIsConnecting(false);
shouldNotReconnectRef.current = true;
addLog({
type: "error",
stage: "connection",
message: t("terminal.maxReconnectAttemptsReached"),
});
return;
}
@@ -538,12 +608,14 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
reconnectAttempts.current++;
toast.info(
t("terminal.reconnecting", {
addLog({
type: "info",
stage: "connection",
message: t("terminal.reconnecting", {
attempt: reconnectAttempts.current,
max: maxReconnectAttempts,
}),
);
});
const delay = Math.min(
2000 * Math.pow(2, reconnectAttempts.current - 1),
@@ -551,6 +623,8 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
);
reconnectTimeoutRef.current = setTimeout(() => {
reconnectTimeoutRef.current = null;
if (
isUnmountingRef.current ||
shouldNotReconnectRef.current ||
@@ -569,7 +643,14 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
if (!jwtToken || jwtToken.trim() === "") {
console.warn("Reconnection cancelled - no authentication token");
isReconnectingRef.current = false;
setConnectionError("Authentication required for reconnection");
updateConnectionError(t("terminal.authenticationRequired"));
setIsConnecting(false);
shouldNotReconnectRef.current = true;
addLog({
type: "error",
stage: "auth",
message: t("terminal.authenticationRequired"),
});
return;
}
@@ -594,6 +675,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
if (!isReconnectingRef.current) {
reconnectAttempts.current = 0;
shouldNotReconnectRef.current = false;
}
const isDev =
@@ -609,7 +691,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
console.error("No JWT token available for WebSocket connection");
setIsConnected(false);
setIsConnecting(false);
setConnectionError("Authentication required");
updateConnectionError("Authentication required");
isConnectingRef.current = false;
return;
}
@@ -627,7 +709,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const wsHost = baseUrl.replace(/^https?:\/\//, "");
return `${wsProtocol}${wsHost}/ssh/websocket/`;
})()
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
: `/ssh/websocket/`;
if (
webSocketRef.current &&
@@ -650,7 +732,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const ws = new WebSocket(wsUrl);
webSocketRef.current = ws;
wasDisconnectedBySSH.current = false;
setConnectionError(null);
updateConnectionError(null);
shouldNotReconnectRef.current = false;
isReconnectingRef.current = false;
setIsConnecting(true);
@@ -665,21 +747,30 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
) {
ws.addEventListener("open", () => {
connectionTimeoutRef.current = setTimeout(() => {
if (!isConnected && !totpRequired && !isPasswordPrompt) {
if (
!isConnected &&
!totpRequired &&
!isPasswordPrompt &&
!connectionErrorRef.current
) {
if (terminal) {
terminal.clear();
}
toast.error(t("terminal.connectionTimeout"));
const timeoutMessage = t("terminal.connectionTimeout");
updateConnectionError(timeoutMessage);
addLog({
type: "error",
stage: "connection",
message: timeoutMessage,
});
if (webSocketRef.current) {
webSocketRef.current.close();
}
if (reconnectAttempts.current > 0) {
attemptReconnection();
} else {
setIsConnecting(false);
shouldNotReconnectRef.current = true;
if (onClose) {
onClose();
}
}
}
}, 35000);
@@ -716,7 +807,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
terminal.write(outputData);
const sudoPasswordPattern =
/(?:\[sudo\] password for \S+:|sudo: a password is required)/;
/(?:\[sudo\][^\n]*:\s*$|sudo:[^\n]*password[^\n]*required)/i;
const passwordToFill =
hostConfig.terminalConfig?.sudoPassword || hostConfig.password;
if (
@@ -746,6 +837,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
},
t("common.confirm"),
t("common.cancel"),
{ confirmOnEnter: true },
);
setTimeout(() => {
sudoPromptShownRef.current = false;
@@ -765,21 +857,24 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
} else if (msg.type === "error") {
const errorMessage = msg.message || t("terminal.unknownError");
addLog({
type: "error",
stage: "connection",
message: errorMessage,
});
if (
errorMessage.toLowerCase().includes("connection") ||
errorMessage.toLowerCase().includes("timeout") ||
errorMessage.toLowerCase().includes("network")
) {
toast.error(
t("terminal.connectionError", { message: errorMessage }),
);
updateConnectionError(errorMessage);
setIsConnected(false);
if (terminal) {
terminal.clear();
}
setIsConnecting(true);
setIsConnecting(false);
wasDisconnectedBySSH.current = false;
attemptReconnection();
return;
}
@@ -792,28 +887,38 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
errorMessage.toLowerCase().includes("key"))) ||
errorMessage.toLowerCase().includes("incorrect password")
) {
toast.error(t("terminal.authError", { message: errorMessage }));
updateConnectionError(errorMessage);
setIsConnecting(false);
shouldNotReconnectRef.current = true;
if (webSocketRef.current) {
webSocketRef.current.close();
}
if (onClose) {
onClose();
}
return;
}
toast.error(t("terminal.error", { message: errorMessage }));
updateConnectionError(errorMessage);
setIsConnecting(false);
} else if (msg.type === "connected") {
setIsConnected(true);
setIsConnecting(false);
isConnectingRef.current = false;
updateConnectionError(null);
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
if (reconnectAttempts.current > 0) {
toast.success(t("terminal.reconnected"));
addLog({
type: "success",
stage: "connection",
message: t("terminal.reconnected"),
});
} else {
addLog({
type: "success",
stage: "connection",
message: t("terminal.connected"),
});
}
reconnectAttempts.current = 0;
isReconnectingRef.current = false;
@@ -882,7 +987,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
onClose();
}
} else if (msg.type === "totp_required") {
totpAttemptsRef.current = 0;
setTotpRequired(true);
setTotpPrompt(msg.prompt || t("terminal.totpCodeLabel"));
setIsPasswordPrompt(false);
@@ -895,20 +999,12 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}
totpTimeoutRef.current = setTimeout(() => {
setTotpRequired(false);
toast.error(t("terminal.totpTimeout"));
if (webSocketRef.current) {
webSocketRef.current.close();
}
}, 180000);
} else if (msg.type === "totp_retry") {
totpAttemptsRef.current++;
const attemptsRemaining =
msg.attempts_remaining || 3 - totpAttemptsRef.current;
toast.error(
`Invalid code. ${attemptsRemaining} ${attemptsRemaining === 1 ? "attempt" : "attempts"} remaining.`,
);
} else if (msg.type === "password_required") {
totpAttemptsRef.current = 0;
setTotpRequired(true);
setTotpPrompt(msg.prompt || t("common.password"));
setIsPasswordPrompt(true);
@@ -921,11 +1017,27 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}
totpTimeoutRef.current = setTimeout(() => {
setTotpRequired(false);
toast.error(t("terminal.passwordTimeout"));
if (webSocketRef.current) {
webSocketRef.current.close();
}
}, 180000);
} else if (msg.type === "warpgate_auth_required") {
setWarpgateAuthRequired(true);
setWarpgateAuthUrl(msg.url || "");
setWarpgateSecurityKey(msg.securityKey || "N/A");
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
if (warpgateTimeoutRef.current) {
clearTimeout(warpgateTimeoutRef.current);
}
warpgateTimeoutRef.current = setTimeout(() => {
setWarpgateAuthRequired(false);
if (webSocketRef.current) {
webSocketRef.current.close();
}
}, 300000);
} else if (msg.type === "keyboard_interactive_available") {
setKeyboardInteractiveDetected(true);
setIsConnecting(false);
@@ -941,9 +1053,18 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
} else if (msg.type === "connection_log") {
if (msg.data) {
addLog({
type: msg.data.level || "info",
stage: msg.data.stage || "auth",
message: msg.data.message,
details: msg.data.details,
});
}
}
} catch {
toast.error(t("terminal.messageParseError"));
// Message parse errors are logged via backend
}
});
@@ -965,9 +1086,29 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
totpTimeoutRef.current = null;
}
if (event.code === 1006) {
console.error(
"[WebSocket] Abnormal closure detected - possible HTTPS/proxy issue",
);
addLog({
type: "error",
stage: "connection",
message: t("terminal.websocketAbnormalClose"),
});
updateConnectionError(t("terminal.websocketAbnormalClose"));
setIsConnecting(false);
shouldNotReconnectRef.current = true;
return;
}
if (event.code === 1008) {
console.error("WebSocket authentication failed:", event.reason);
setConnectionError("Authentication failed - please re-login");
addLog({
type: "error",
stage: "auth",
message: "Authentication failed - please re-login",
});
updateConnectionError("Authentication failed - please re-login");
setIsConnecting(false);
shouldNotReconnectRef.current = true;
@@ -980,26 +1121,47 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
return;
}
setIsConnecting(false);
if (
!isConnected &&
event.wasClean &&
(event.code === 1005 || event.code === 1000)
) {
console.error("[WebSocket] Connection rejected by server");
addLog({
type: "error",
stage: "connection",
message: t("terminal.connectionRejected"),
});
updateConnectionError(t("terminal.connectionRejected"));
setIsConnecting(false);
shouldNotReconnectRef.current = true;
return;
}
const shouldAttemptReconnection =
!wasDisconnectedBySSH.current &&
!isUnmountingRef.current &&
!shouldNotReconnectRef.current &&
!isConnectingRef.current
) {
!isConnectingRef.current;
if (shouldAttemptReconnection) {
wasDisconnectedBySSH.current = false;
attemptReconnection();
} else {
setIsConnecting(false);
}
});
ws.addEventListener("error", () => {
ws.addEventListener("error", (event) => {
if (currentAttemptId !== connectionAttemptIdRef.current) {
return;
}
console.error("[WebSocket] Error:", event);
setIsConnected(false);
isConnectingRef.current = false;
setConnectionError(t("terminal.websocketError"));
updateConnectionError(t("terminal.websocketError"));
if (terminal) {
terminal.clear();
}
@@ -1009,15 +1171,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
clearTimeout(totpTimeoutRef.current);
totpTimeoutRef.current = null;
}
if (
!isUnmountingRef.current &&
!shouldNotReconnectRef.current &&
!isConnectingRef.current
) {
wasDisconnectedBySSH.current = false;
attemptReconnection();
}
});
}
@@ -1160,7 +1313,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
fontSize: config.fontSize,
fontFamily,
allowTransparency: true,
convertEol: true,
convertEol: false,
windowsMode: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
@@ -1314,16 +1467,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
resizeObserver.observe(xtermRef.current);
return () => {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
setIsConnecting(false);
isFittingRef.current = false;
resizeObserver.disconnect();
element?.removeEventListener("contextmenu", handleContextMenu);
element?.removeEventListener("keydown", handleMacKeyboard, true);
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
};
}, [xtermRef, terminal, hostConfig, isDarkMode]);
useEffect(() => {
return () => {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
setIsConnecting(false);
if (reconnectTimeoutRef.current)
clearTimeout(reconnectTimeoutRef.current);
if (connectionTimeoutRef.current)
@@ -1335,7 +1493,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}
webSocketRef.current?.close();
};
}, [xtermRef, terminal, hostConfig, isDarkMode]);
}, []);
useEffect(() => {
if (!terminal) return;
@@ -1345,6 +1503,25 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
return true;
}
if (e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey) {
const key = e.key.toLowerCase();
const blockedKeys = ["w", "t", "n", "q"];
if (blockedKeys.includes(key)) {
e.preventDefault();
e.stopPropagation();
const ctrlCode = key.charCodeAt(0) - 96;
if (webSocketRef.current?.readyState === 1) {
webSocketRef.current.send(
JSON.stringify({
type: "input",
data: String.fromCharCode(ctrlCode),
}),
);
}
return false;
}
}
if (showAutocompleteRef.current) {
if (e.key === "Escape") {
e.preventDefault();
@@ -1526,6 +1703,14 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
if (!terminal || !hostConfig || !isVisible) return;
if (isConnected || isConnecting) return;
if (isReconnectingRef.current || reconnectTimeoutRef.current !== null) {
return;
}
if (shouldNotReconnectRef.current) {
return;
}
if (terminal.cols < 10 || terminal.rows < 3) {
requestAnimationFrame(() => {
if (terminal.cols > 0 && terminal.rows > 0) {
@@ -1544,7 +1729,11 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
scheduleNotify(terminal.cols, terminal.rows);
connectToHost(terminal.cols, terminal.rows);
}
}, [terminal, hostConfig, isVisible, isConnected, isConnecting]);
// Note: Using hostConfig.id instead of hostConfig object to prevent
// unnecessary reconnections when host properties are updated.
// Only reconnect when switching to a different host.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [terminal, hostConfig.id, isVisible, isConnected, isConnecting]);
useEffect(() => {
if (!terminal || !fitAddonRef.current || !isVisible) return;
@@ -1561,6 +1750,8 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
return () => clearTimeout(fitTimeoutId);
}, [terminal, isVisible, splitScreen, isConnecting]);
const hasConnectionError = !!connectionError;
return (
<div className="h-full w-full relative" style={{ backgroundColor }}>
<div
@@ -1568,7 +1759,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
className="h-full w-full"
style={{
pointerEvents: isVisible ? "auto" : "none",
visibility: isConnecting || !isFitted ? "hidden" : "visible",
visibility:
isConnected && isFitted && !connectionError
? "visible"
: "hidden",
}}
onClick={() => {
if (terminal && !splitScreen) {
@@ -1577,6 +1771,19 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}}
/>
<SimpleLoader
visible={isConnecting && !isConnectionLogExpanded}
message={t("terminal.connecting")}
backgroundColor={backgroundColor}
/>
<ConnectionLog
isConnecting={isConnecting}
isConnected={isConnected}
hasConnectionError={hasConnectionError}
position={hasConnectionError ? "top" : "bottom"}
/>
<TOTPDialog
isOpen={totpRequired}
prompt={totpPrompt}
@@ -1599,6 +1806,16 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
backgroundColor={backgroundColor}
/>
<WarpgateDialog
isOpen={warpgateAuthRequired}
url={warpgateAuthUrl}
securityKey={warpgateSecurityKey}
onContinue={handleWarpgateContinue}
onCancel={handleWarpgateCancel}
onOpenUrl={handleWarpgateOpenUrl}
backgroundColor={backgroundColor}
/>
<CommandAutocomplete
visible={showAutocomplete}
suggestions={autocompleteSuggestions}
@@ -1606,17 +1823,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
position={autocompletePosition}
onSelect={handleAutocompleteSelect}
/>
<SimpleLoader
visible={isConnecting}
message={t("terminal.connecting")}
backgroundColor={backgroundColor}
/>
</div>
);
},
);
export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
function Terminal(props, ref) {
return (
<ConnectionLogProvider>
<TerminalInner {...props} ref={ref} />
</ConnectionLogProvider>
);
},
);
const style = document.createElement("style");
style.innerHTML = `
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap');
@@ -1655,7 +1876,6 @@ style.innerHTML = `
font-display: swap;
}
/* Light theme scrollbars */
.xterm .xterm-viewport::-webkit-scrollbar {
width: 8px;
background: transparent;
@@ -1672,7 +1892,6 @@ style.innerHTML = `
scrollbar-color: rgba(0,0,0,0.3) transparent;
}
/* Dark theme scrollbars */
.dark .xterm .xterm-viewport::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.3);
}