* feat: AI translations at release, 7 new languages and regional fallback

* fix: proper plural forms for count strings and full translations

* fix: plural forms for selected, hidden and active counts

* fix: synced plugin updates failing on desktop and server-only plugins shown as stopped

* fix: restore rounded corners on app icons and favicon

* feat: desktop app auto updates with automatic, ask first or never modes

* feat: community and custom plugin registries

* feat: plugin guide for AI tools in the SDK with a lint check

* fix: keep the terminal WebSocket open past nginx's 60s default (#1588)

* chore: update changelog

* feat: show plugin download counts only and stop CI builds inflating them

* feat: hide installed plugins in the browse tab by default

* chore: pin bundled plugins to latest releases

* chore: update translations

* fix: focusing a saved password field and saving no longer erases it

* fix: tell desktop users when their 2.8 data needs 2.9 first

* fix: point windows update info at the x64 installer

* fix: keep desktop client tunnel sockets alive behind proxies

* chore: pin bundled plugins to latest releases

* fix: cover client tunnel keepalive in the c2s runtime tests

* fix: typing after opening a host no longer goes to the host search

* fix: accept tab and = separators in SSH config import (#1598)

* chore: update changelog

* chore: drop plugin versions from core changelog

* chore: update translations

* chore: lint, format, and bump version to 26.10.1

---------

Co-authored-by: Darnel Hunter <68697450+innotelinc@users.noreply.github.com>
Co-authored-by: Iain <Iain1970@protonmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 11:35:48 -05:00
1 parent 6873bc92bb
commit 62ac931468
174 files changed
+94334 -39995

No files matched your search

+74
View File
@@ -29,6 +29,7 @@ const pty = require("node-pty");
const WebSocket = require("ws");
const linkedServer = require("./linked-server.cjs");
const desktopSettings = require("./desktop-settings.cjs");
const autoUpdates = require("./auto-updater.cjs");
const { launchNativeRdp } = require("./native-rdp.cjs");
const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs");
const { quitApp } = require("./app-quit.cjs");
@@ -1752,6 +1753,65 @@ ipcMain.handle("save-desktop-settings", (_event, settings) => {
return desktopSettings.saveDesktopSettings(settings);
});
let appUpdater = null;
function startAppUpdater() {
const kind = autoUpdates.getInstallKind({
platform: process.platform,
arch: process.arch,
isPackaged: app.isPackaged,
mas: !!process.mas,
env: process.env,
execPath: process.execPath,
resourcesPath: process.resourcesPath,
exists: (target) => fs.existsSync(target),
readFile: (target) => fs.readFileSync(target, "utf8"),
});
logToFile("Updater install kind:", kind);
appUpdater = autoUpdates.createUpdater({
currentVersion: app.getVersion(),
platform: process.platform,
arch: process.arch,
kind,
getMode: () => desktopSettings.getDesktopSettings().updateMode,
saveMode: (mode) =>
desktopSettings.saveDesktopSettings({ updateMode: mode }),
fetchReleases: async () => {
const { net } = require("electron");
const response = await net.fetch(autoUpdates.RELEASES_API, {
headers: { Accept: "application/vnd.github+json" },
});
if (!response.ok) {
throw new Error(`GitHub releases returned ${response.status}`);
}
return response.json();
},
loadAutoUpdater: () => require("electron-updater").autoUpdater,
send: (state) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("desktop-update-state", state);
}
},
log: logToFile,
beforeInstall: () => {
isQuitting = true;
},
});
appUpdater.start();
}
ipcMain.handle(
"desktop-update-get-state",
() => appUpdater?.getState() ?? null,
);
ipcMain.handle("desktop-update-check", () => appUpdater?.check() ?? null);
ipcMain.handle("desktop-update-download", () => appUpdater?.download() ?? null);
ipcMain.handle("desktop-update-install", () => appUpdater?.install() ?? false);
ipcMain.handle(
"desktop-update-set-mode",
(_event, mode) => appUpdater?.setMode(mode) ?? null,
);
// The server an older version of the app was pointed at, offered again when
// linking so upgraded installs do not have to look it up.
ipcMain.handle("get-previous-server-url", () => {
@@ -1924,6 +1984,16 @@ const C2S_WS_HIGH_WATERMARK = 1024 * 1024;
const C2S_WS_LOW_WATERMARK = 256 * 1024;
const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024;
// A proxy in front of the server may drop an idle WebSocket, which stopped
// the tunnel. Pings count as traffic.
function keepC2SSocketAlive(ws) {
const timer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.ping();
}, 25_000);
timer.unref?.();
ws.once("close", () => clearInterval(timer));
}
// C2S (client-to-server) tunnels relay through the server this desktop is
// linked to, not the embedded backend.
function getC2SRemoteBaseUrl() {
@@ -2192,6 +2262,7 @@ async function openC2SRelay(
getWebSocketOptions(relayUrl, { headers }),
);
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
keepC2SSocketAlive(ws);
const pendingChunks = [];
let ready = false;
let closed = false;
@@ -2299,6 +2370,7 @@ async function testC2SRelay(
getWebSocketOptions(relayUrl, { headers }),
);
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
keepC2SSocketAlive(ws);
if (runtime) runtime.authSocket = ws;
return new Promise((resolve) => {
@@ -2609,6 +2681,7 @@ async function startC2SRemoteTunnel(tunnel, index = 0, authToken) {
getWebSocketOptions(relayUrl, { headers }),
);
attachC2SAuth(ws, tunnel.name || getC2STunnelName(tunnel));
keepC2SSocketAlive(ws);
const sockets = new Map();
let closed = false;
@@ -3804,6 +3877,7 @@ app.whenReady().then(async () => {
createTray();
createWindow();
startAppUpdater();
desktopSettings.removeOldSyncFiles();
logToFile("=== Startup complete ===");
});