* 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

+21 -1
View File
@@ -1,10 +1,19 @@
import { LayoutTemplate, Monitor, Palette, Puzzle, Shield } from "lucide-react";
import {
LayoutTemplate,
Monitor,
MonitorDown,
Palette,
Puzzle,
Shield,
} from "lucide-react";
import { CORE_ONBOARDING_STEPS } from "@/types/onboarding";
import { PluginsStep } from "./steps/PluginsStep";
import { PresetStep } from "./steps/PresetStep";
import { AppearanceStep } from "./steps/AppearanceStep";
import { SecurityStep } from "./steps/SecurityStep";
import { DesktopSyncStep } from "./steps/DesktopSyncStep";
import { DesktopUpdatesStep } from "./steps/DesktopUpdatesStep";
import { desktopUpdates } from "@/lib/desktop-updates";
import type { OnboardingStepDef } from "./onboarding-registry";
/**
@@ -38,6 +47,17 @@ export const CORE_STEPS: OnboardingStepDef[] = [
position: 20,
isRelevant: (ctx) => ctx.isDesktop,
},
{
id: "desktop-updates",
version: CORE_ONBOARDING_STEPS["desktop-updates"],
titleKey: "desktopUpdates.onboardingTitle",
icon: MonitorDown,
Component: DesktopUpdatesStep,
audience: "all",
position: 25,
// App Store builds have no updater.
isRelevant: (ctx) => ctx.isDesktop && desktopUpdates() !== null,
},
{
id: "preset",
version: CORE_ONBOARDING_STEPS.preset,
@@ -0,0 +1,79 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { BellRing, Download, PauseCircle } from "lucide-react";
import {
desktopUpdates,
useDesktopUpdate,
type DesktopUpdateMode,
} from "@/lib/desktop-updates";
/**
* Desktop only: how the app updates itself. Can be changed later in
* Settings, Updates.
*/
export function DesktopUpdatesStep() {
const { t } = useTranslation();
const state = useDesktopUpdate();
const [picked, setPicked] = useState<DesktopUpdateMode | null>(null);
const mode = picked ?? state?.mode ?? "prompt";
const choose = (next: DesktopUpdateMode) => {
setPicked(next);
void desktopUpdates()
?.setMode(next)
.catch(() => {});
};
const options = [
{
id: "automatic" as const,
icon: Download,
title: t("desktopUpdates.modeAutomatic"),
body: t("desktopUpdates.automaticBody"),
},
{
id: "prompt" as const,
icon: BellRing,
title: t("desktopUpdates.modePrompt"),
body: t("desktopUpdates.promptBody"),
},
{
id: "never" as const,
icon: PauseCircle,
title: t("desktopUpdates.modeNever"),
body: t("desktopUpdates.neverBody"),
},
];
return (
<div className="flex flex-col gap-3">
<p className="text-xs text-muted-foreground">
{state?.kind === "manual"
? t("desktopUpdates.onboardingManual")
: t("desktopUpdates.onboardingBody")}
</p>
<div className="flex flex-col gap-1.5">
{options.map(({ id, icon: Icon, title, body }) => (
<button
key={id}
type="button"
onClick={() => choose(id)}
className={`flex items-start gap-2.5 border p-2.5 text-left transition-colors ${
mode === id
? "border-accent-brand bg-accent-brand/10"
: "border-border bg-card hover:bg-muted/40"
}`}
>
<Icon size={14} className="mt-0.5 shrink-0 text-accent-brand" />
<span className="flex flex-col gap-1">
<span className="text-xs font-medium">{title}</span>
<span className="text-[10px] leading-snug text-muted-foreground">
{body}
</span>
</span>
</button>
))}
</div>
</div>
);
}