From 2ee1318ded4a89dd0e3332323b60ca0c4d361720 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 31 Dec 2025 22:54:32 -0600 Subject: [PATCH 1/8] fix: electron build errors and skip macos job --- .github/workflows/electron.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index 2fcd3d55..9c15ce00 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -356,7 +356,7 @@ jobs: build-macos: runs-on: macos-latest - if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all' + if: (github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all') && github.event.inputs.artifact_destination != 'submit' needs: [] permissions: contents: write @@ -801,11 +801,20 @@ jobs: URL="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" mkdir -p release_asset - PATH="release_asset/$DMG_NAME" + DOWNLOAD_PATH="release_asset/$DMG_NAME" echo "Downloading DMG from $URL" - curl -L -o "$PATH" "$URL" - CHECKSUM=$(shasum -a 256 "$PATH" | awk '{print $1}') + if command -v curl &> /dev/null; then + curl -L -o "$DOWNLOAD_PATH" "$URL" + elif command -v wget &> /dev/null; then + wget -O "$DOWNLOAD_PATH" "$URL" + else + echo "Neither curl nor wget is available, installing curl" + brew install curl + curl -L -o "$DOWNLOAD_PATH" "$URL" + fi + + CHECKSUM=$(shasum -a 256 "$DOWNLOAD_PATH" | awk '{print $1}') echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT -- 2.49.1 From 6feb8405cec60b247ba0aaa7575daf1efe81d44b Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 31 Dec 2025 23:29:46 -0600 Subject: [PATCH 2/8] fix: testflight submit failure --- .github/workflows/electron.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index 9c15ce00..e7468528 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -986,7 +986,7 @@ jobs: - name: Deploy to App Store Connect (TestFlight) if: steps.check_asc_creds.outputs.has_credentials == 'true' run: | - PKG_FILE=$(find artifact-mas -name "*.pkg" -type f | head -n 1) + PKG_FILE=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1) if [ -z "$PKG_FILE" ]; then echo "PKG file not found, exiting." exit 1 -- 2.49.1 From bdf9ea282e944d858be699e45266535e43c34e60 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 31 Dec 2025 23:33:17 -0600 Subject: [PATCH 3/8] fix: made submit job match build type --- .github/workflows/electron.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index e7468528..7c087e03 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -584,7 +584,7 @@ jobs: submit-to-chocolatey: runs-on: windows-latest - if: github.event.inputs.artifact_destination == 'submit' + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '') permissions: contents: read @@ -689,7 +689,7 @@ jobs: submit-to-flatpak: runs-on: ubuntu-latest - if: github.event.inputs.artifact_destination == 'submit' + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '') needs: [] permissions: contents: read @@ -776,7 +776,7 @@ jobs: submit-to-homebrew: runs-on: macos-latest - if: github.event.inputs.artifact_destination == 'submit' + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos') needs: [] permissions: contents: read @@ -881,7 +881,7 @@ jobs: submit-to-testflight: runs-on: macos-latest - if: github.event.inputs.artifact_destination == 'submit' + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos') needs: [] permissions: contents: write -- 2.49.1 From 5168ded79d9e9d9446bb17fe37a365dcdf6214c0 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:52:38 -0600 Subject: [PATCH 4/8] Update Crowdin configuration file --- crowdin.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..9900c0d1 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /src/locales/en.json + translation: /src/locales/%two_letters_code%.json -- 2.49.1 From fceb430d22950ca2305ddb26d5e425927118a667 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:57:13 -0600 Subject: [PATCH 5/8] Update Crowdin configuration file --- crowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdin.yml b/crowdin.yml index 9900c0d1..51095a4b 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,3 @@ files: - source: /src/locales/en.json - translation: /src/locales/%two_letters_code%.json + translation: /src/locales/translated/%two_letters_code%.json -- 2.49.1 From fc87146e4bba73a1e6729daeac4e59acbdd61a74 Mon Sep 17 00:00:00 2001 From: Gaylord Julien Date: Fri, 2 Jan 2026 01:42:51 +0100 Subject: [PATCH 6/8] Update Linux Portable section with AUR link (#474) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ddf04d6..d01a3aab 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Supported Devices: - MSI Installer - Chocolatey Package Manager - Linux (x64/ia32) - - Portable + - Portable [(AUR available)](https://aur.archlinux.org/packages/termix-bin) - AppImage - Deb - Flatpak -- 2.49.1 From 1a2179c3457df5d513cee7f889b44a074526530a Mon Sep 17 00:00:00 2001 From: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:43:38 -0600 Subject: [PATCH 7/8] fix: resolve Vite build warnings for mixed static/dynamic imports (#473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude --- crowdin.yml | 3 + package-lock.json | 60 +++++++++++++------ src/ui/desktop/DesktopApp.tsx | 3 +- .../dashboard/apps/alerts/AlertManager.tsx | 2 +- .../apps/features/docker/DockerManager.tsx | 3 +- .../features/server-stats/ServerStats.tsx | 5 +- .../apps/features/terminal/Terminal.tsx | 10 ++-- .../apps/features/tunnel/TunnelManager.tsx | 3 +- .../credentials/CredentialSelector.tsx | 2 +- .../host-manager/hosts/HostManagerEditor.tsx | 3 +- .../host-manager/hosts/HostManagerViewer.tsx | 2 +- src/ui/desktop/navigation/hosts/Host.tsx | 3 +- src/ui/desktop/user/UserProfile.tsx | 2 +- src/ui/main-axios.ts | 5 +- vite.config.ts | 11 ++++ 15 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..51095a4b --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /src/locales/en.json + translation: /src/locales/translated/%two_letters_code%.json diff --git a/package-lock.json b/package-lock.json index 1c9d6013..47d466f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -158,6 +158,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -443,6 +444,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz", "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -491,6 +493,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -517,6 +520,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -544,6 +548,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -744,6 +749,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -820,6 +826,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -841,6 +848,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1168,6 +1176,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1554,7 +1563,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1576,7 +1584,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2623,7 +2630,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@lezer/cpp": { "version": "1.1.3", @@ -2663,6 +2671,7 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -2694,6 +2703,7 @@ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", @@ -2716,6 +2726,7 @@ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -5163,6 +5174,7 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -5320,6 +5332,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -5442,6 +5455,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5484,6 +5498,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5494,6 +5509,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5661,6 +5677,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -6068,7 +6085,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/7zip-bin": { "version": "5.2.0", @@ -6103,6 +6121,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6530,6 +6549,7 @@ "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -6672,6 +6692,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7671,6 +7692,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7747,8 +7769,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -8211,6 +8232,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -8308,8 +8330,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/dot-prop": { "version": "5.3.0", @@ -8684,7 +8705,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8705,7 +8725,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8721,7 +8740,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -8732,7 +8750,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -8994,6 +9011,7 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11005,6 +11023,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -12571,7 +12590,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -14601,7 +14619,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -14619,7 +14636,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -15105,6 +15121,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15114,6 +15131,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15140,6 +15158,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -15287,6 +15306,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -15495,7 +15515,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -16885,7 +16906,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -16926,7 +16946,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -16941,7 +16960,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -17046,6 +17064,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17251,6 +17270,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17663,6 +17683,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -17754,6 +17775,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 77d1d7ba..6fc9ac98 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -13,7 +13,7 @@ import { AdminSettings } from "@/ui/desktop/apps/admin/AdminSettings.tsx"; import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx"; import { Toaster } from "@/components/ui/sonner.tsx"; import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx"; -import { getUserInfo } from "@/ui/main-axios.ts"; +import { getUserInfo, logoutUser, isElectron } from "@/ui/main-axios.ts"; import { useTheme } from "@/components/theme-provider"; function AppContent() { @@ -163,7 +163,6 @@ function AppContent() { setTimeout(async () => { try { - const { logoutUser, isElectron } = await import("@/ui/main-axios.ts"); await logoutUser(); if (isElectron()) { diff --git a/src/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx b/src/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx index 16037a07..405d0c0e 100644 --- a/src/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx +++ b/src/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button.tsx"; import { getUserAlerts, dismissAlert } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import type { TermixAlert } from "../../../../../../types"; +import { toast } from "sonner"; interface AlertManagerProps { userId: string | null; @@ -53,7 +54,6 @@ export function AlertManager({ setAlerts(sortedAlerts); setCurrentAlertIndex(0); } catch { - const { toast } = await import("sonner"); toast.error(t("homepage.failedToLoadAlerts")); setError(t("homepage.failedToLoadAlerts")); } finally { diff --git a/src/ui/desktop/apps/features/docker/DockerManager.tsx b/src/ui/desktop/apps/features/docker/DockerManager.tsx index eba3b727..c73a5789 100644 --- a/src/ui/desktop/apps/features/docker/DockerManager.tsx +++ b/src/ui/desktop/apps/features/docker/DockerManager.tsx @@ -18,6 +18,7 @@ import { keepaliveDockerSession, verifyDockerTOTP, logActivity, + getSSHHosts, } from "@/ui/main-axios.ts"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { AlertCircle } from "lucide-react"; @@ -121,7 +122,6 @@ export function DockerManager({ const fetchLatestHostConfig = async () => { if (hostConfig?.id) { try { - const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { @@ -138,7 +138,6 @@ export function DockerManager({ const handleHostsChanged = async () => { if (hostConfig?.id) { try { - const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { diff --git a/src/ui/desktop/apps/features/server-stats/ServerStats.tsx b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx index 8b516d12..a87813b5 100644 --- a/src/ui/desktop/apps/features/server-stats/ServerStats.tsx +++ b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx @@ -11,6 +11,8 @@ import { submitMetricsTOTP, executeSnippet, logActivity, + sendMetricsHeartbeat, + getSSHHosts, type ServerMetrics, } from "@/ui/main-axios.ts"; import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx"; @@ -145,7 +147,6 @@ export function ServerStats({ const heartbeatInterval = setInterval(async () => { try { - const { sendMetricsHeartbeat } = await import("@/ui/main-axios.ts"); await sendMetricsHeartbeat(viewerSessionId); } catch (error) { console.error("Failed to send heartbeat:", error); @@ -273,7 +274,6 @@ export function ServerStats({ const fetchLatestHostConfig = async () => { if (hostConfig?.id) { try { - const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { @@ -290,7 +290,6 @@ export function ServerStats({ const handleHostsChanged = async () => { if (hostConfig?.id) { try { - const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { diff --git a/src/ui/desktop/apps/features/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx index d6081572..5fa0a1c8 100644 --- a/src/ui/desktop/apps/features/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -18,6 +18,8 @@ import { isElectron, logActivity, getSnippets, + deleteCommandFromHistory, + getCommandHistory, } from "@/ui/main-axios.ts"; import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx"; import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx"; @@ -212,8 +214,7 @@ export const Terminal = forwardRef( if (showHistoryDialog && hostConfig.id) { setIsLoadingHistory(true); setIsLoadingRef.current(true); - import("@/ui/main-axios.ts") - .then((module) => module.getCommandHistory(hostConfig.id!)) + getCommandHistory(hostConfig.id!) .then((history) => { setCommandHistory(history); setCommandHistoryContextRef.current(history); @@ -235,8 +236,7 @@ export const Terminal = forwardRef( localStorage.getItem("commandAutocomplete") === "true"; if (hostConfig.id && autocompleteEnabled) { - import("@/ui/main-axios.ts") - .then((module) => module.getCommandHistory(hostConfig.id!)) + getCommandHistory(hostConfig.id!) .then((history) => { autocompleteHistory.current = history; }) @@ -1107,8 +1107,6 @@ export const Terminal = forwardRef( if (!hostConfig.id) return; try { - const { deleteCommandFromHistory } = - await import("@/ui/main-axios.ts"); await deleteCommandFromHistory(hostConfig.id, command); setCommandHistory((prev) => { diff --git a/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx b/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx index ea7346cc..ef0c7eb8 100644 --- a/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx +++ b/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx @@ -3,6 +3,7 @@ import { useSidebar } from "@/components/ui/sidebar.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { Tunnel } from "@/ui/desktop/apps/features/tunnel/Tunnel.tsx"; import { useTranslation } from "react-i18next"; +import { getSSHHosts } from "@/ui/main-axios.ts"; interface HostConfig { id: number; @@ -44,7 +45,6 @@ export function TunnelManager({ const fetchLatestHostConfig = async () => { if (hostConfig?.id) { try { - const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { @@ -61,7 +61,6 @@ export function TunnelManager({ const handleHostsChanged = async () => { if (hostConfig?.id) { try { - const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === hostConfig.id); if (updatedHost) { diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx index 8be74967..77ac5a30 100644 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx @@ -5,6 +5,7 @@ import { FormControl, FormItem, FormLabel } from "@/components/ui/form.tsx"; import { getCredentials } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import type { Credential } from "../../../../../types"; +import { toast } from "sonner"; interface CredentialSelectorProps { value?: number | null; @@ -36,7 +37,6 @@ export function CredentialSelector({ : data.credentials || data.data || []; setCredentials(credentialsArray); } catch { - const { toast } = await import("sonner"); toast.error(t("credentials.failedToFetchCredentials")); setCredentials([]); } finally { diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx index 1f6796ba..df3ec54d 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx @@ -52,6 +52,7 @@ import { getHostAccess, revokeHostAccess, getSSHHostById, + notifyHostCreatedOrUpdated, type Role, type AccessRecord, } from "@/ui/main-axios.ts"; @@ -819,8 +820,6 @@ export function HostManagerEditor({ window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); if (savedHost?.id) { - const { notifyHostCreatedOrUpdated } = - await import("@/ui/main-axios.ts"); notifyHostCreatedOrUpdated(savedHost.id); } } catch (error) { diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx index 115696b0..fa53fb20 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx @@ -26,6 +26,7 @@ import { updateFolderMetadata, deleteAllHostsInFolder, getServerStatusById, + refreshServerPolling, } from "@/ui/main-axios.ts"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -313,7 +314,6 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) { await fetchHosts(); window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - const { refreshServerPolling } = await import("@/ui/main-axios.ts"); refreshServerPolling(); } catch { toast.error(t("hosts.failedToDeleteHost")); diff --git a/src/ui/desktop/navigation/hosts/Host.tsx b/src/ui/desktop/navigation/hosts/Host.tsx index 95f16cdb..bc013311 100644 --- a/src/ui/desktop/navigation/hosts/Host.tsx +++ b/src/ui/desktop/navigation/hosts/Host.tsx @@ -18,7 +18,7 @@ import { DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext"; -import { getServerStatusById } from "@/ui/main-axios"; +import { getServerStatusById, getSSHHosts } from "@/ui/main-axios"; import type { HostProps } from "../../../../types"; import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; import { useTranslation } from "react-i18next"; @@ -47,7 +47,6 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement { useEffect(() => { const handleHostsChanged = async () => { - const { getSSHHosts } = await import("@/ui/main-axios.ts"); const hosts = await getSSHHosts(); const updatedHost = hosts.find((h) => h.id === host.id); if (updatedHost) { diff --git a/src/ui/desktop/user/UserProfile.tsx b/src/ui/desktop/user/UserProfile.tsx index ebb3f83f..61cd61c6 100644 --- a/src/ui/desktop/user/UserProfile.tsx +++ b/src/ui/desktop/user/UserProfile.tsx @@ -42,6 +42,7 @@ import { PasswordReset } from "@/ui/desktop/user/PasswordReset.tsx"; import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "@/ui/desktop/user/LanguageSwitcher.tsx"; import { useSidebar } from "@/components/ui/sidebar.tsx"; +import { toast } from "sonner"; interface UserProfileProps { isTopbarOpen?: boolean; @@ -144,7 +145,6 @@ export function UserProfile({ const info = await getVersionInfo(); setVersionInfo({ version: info.localVersion }); } catch { - const { toast } = await import("sonner"); toast.error(t("user.failedToLoadVersionInfo")); } }; diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 9e85d92c..a1eed327 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1,4 +1,5 @@ import axios, { AxiosError, type AxiosInstance } from "axios"; +import { toast } from "sonner"; import type { SSHHost, SSHHostData, @@ -446,9 +447,7 @@ function createApiInstance( if (isSessionExpired && typeof window !== "undefined") { console.warn("Session expired - please log in again"); - import("sonner").then(({ toast }) => { - toast.warning("Session expired. Please log in again."); - }); + toast.warning("Session expired. Please log in again."); } } } diff --git a/vite.config.ts b/vite.config.ts index aec8f992..c0fd1337 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,6 +20,17 @@ export default defineConfig({ base: "./", build: { sourcemap: false, + rollupOptions: { + output: { + manualChunks: { + 'react-vendor': ['react', 'react-dom'], + 'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu', '@radix-ui/react-select', '@radix-ui/react-tabs', '@radix-ui/react-switch', '@radix-ui/react-tooltip', '@radix-ui/react-scroll-area', '@radix-ui/react-separator', 'lucide-react', 'clsx', 'tailwind-merge', 'class-variance-authority'], + 'monaco': ['monaco-editor'], + 'codemirror': ['@uiw/react-codemirror', '@codemirror/view', '@codemirror/state', '@codemirror/language', '@codemirror/commands', '@codemirror/search', '@codemirror/autocomplete'], + } + } + }, + chunkSizeWarningLimit: 1000, }, server: { https: useHTTPS -- 2.49.1 From 177e783f92f9053cd900e84a206130a4395e2af0 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 2 Jan 2026 00:32:22 -0600 Subject: [PATCH 8/8] fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --- src/backend/ssh/file-manager.ts | 54 +++++++++++++++++-- .../file-manager/components/FileViewer.tsx | 8 +-- .../file-manager/components/FileWindow.tsx | 30 ++++++++++- src/ui/main-axios.ts | 6 ++- 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 2769ed8c..97f6a88b 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -343,6 +343,27 @@ function getMimeType(fileName: string): string { return mimeTypes[ext || ""] || "application/octet-stream"; } +function detectBinary(buffer: Buffer): boolean { + if (buffer.length === 0) return false; + + const sampleSize = Math.min(buffer.length, 8192); + let nullBytes = 0; + + for (let i = 0; i < sampleSize; i++) { + const byte = buffer[i]; + + if (byte === 0) { + nullBytes++; + } + + if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) { + if (++nullBytes > 1) return true; + } + } + + return nullBytes / sampleSize > 0.01; +} + app.post("/ssh/file_manager/ssh/connect", async (req, res) => { const { sessionId, @@ -1368,11 +1389,11 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { return res.status(500).json({ error: err.message }); } - let data = ""; + let binaryData = Buffer.alloc(0); let errorData = ""; stream.on("data", (chunk: Buffer) => { - data += chunk.toString(); + binaryData = Buffer.concat([binaryData, chunk]); }); stream.stderr.on("data", (chunk: Buffer) => { @@ -1396,7 +1417,23 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { }); } - res.json({ content: data, path: filePath }); + const isBinary = detectBinary(binaryData); + + if (isBinary) { + const base64Content = binaryData.toString("base64"); + res.json({ + content: base64Content, + path: filePath, + encoding: "base64", + }); + } else { + const textContent = binaryData.toString("utf8"); + res.json({ + content: textContent, + path: filePath, + encoding: "utf8", + }); + } }); }); }); @@ -1440,7 +1477,16 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { let fileBuffer; try { if (typeof content === "string") { - fileBuffer = Buffer.from(content, "base64"); + try { + const testBuffer = Buffer.from(content, "base64"); + if (testBuffer.toString("base64") === content) { + fileBuffer = testBuffer; + } else { + fileBuffer = Buffer.from(content, "utf8"); + } + } catch { + fileBuffer = Buffer.from(content, "utf8"); + } } else if (Buffer.isBuffer(content)) { fileBuffer = content; } else { diff --git a/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx b/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx index d3f8455f..e09b5cee 100644 --- a/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx @@ -333,13 +333,7 @@ export function FileViewer({ const ext = fileName.split(".").pop()?.toLowerCase() || ""; if (ext === "svg") { - try { - const base64 = btoa(unescape(encodeURIComponent(content))); - return `data:image/svg+xml;base64,${base64}`; - } catch (e) { - console.error("Failed to encode SVG:", e); - return ""; - } + return `data:image/svg+xml;base64,${content}`; } return `data:image/*;base64,${content}`; diff --git a/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx b/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx index 5768a949..9cf7f25c 100644 --- a/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx @@ -47,6 +47,22 @@ interface FileWindowProps { onFileNotFound?: (file: FileItem) => void; } +function isDisplayableText(str: string): boolean { + let printable = 0; + for (let i = 0; i < Math.min(str.length, 1000); i++) { + const code = str.charCodeAt(i); + if ( + (code >= 32 && code <= 126) || + code === 9 || + code === 10 || + code === 13 + ) { + printable++; + } + } + return printable / Math.min(str.length, 1000) > 0.85; +} + export function FileWindow({ windowId, file, @@ -106,7 +122,19 @@ export function FileWindow({ await ensureSSHConnection(); const response = await readSSHFile(sshSessionId, file.path); - const fileContent = response.content || ""; + let fileContent = response.content || ""; + + if (response.encoding === "base64") { + try { + const decoded = atob(fileContent); + if (isDisplayableText(decoded)) { + fileContent = decoded; + } + } catch (err) { + console.error("Failed to decode base64 content:", err); + } + } + setContent(fileContent); setPendingContent(fileContent); diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 9e85d92c..afe82295 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1421,7 +1421,11 @@ export async function identifySSHSymlink( export async function readSSHFile( sessionId: string, path: string, -): Promise<{ content: string; path: string }> { +): Promise<{ + content: string; + path: string; + encoding?: "base64" | "utf8"; +}> { try { const response = await fileManagerApi.get("/ssh/readFile", { params: { sessionId, path }, -- 2.49.1