* 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

+121 -31
View File
@@ -6,6 +6,7 @@ import {
Columns3,
Download,
Globe,
Library,
Puzzle,
RefreshCw,
RotateCcw,
@@ -40,10 +41,12 @@ import { InlineView, SurfaceScope } from "@/components/surface/surface-scope";
import { cn } from "@/lib/utils";
import {
BetaBadge,
InstallCountFact,
DownloadCount,
PluginIconBox,
SourceBadge,
UnverifiedBadge,
} from "./plugin-bits";
import { RegistriesSection } from "./RegistriesSection";
import { PluginConsentPrompt } from "./PluginConsentPrompt";
import { PluginDetail } from "./PluginDetail";
import {
@@ -60,7 +63,7 @@ import { usePluginsManager, type PluginsManager } from "./use-plugins-manager";
import { docsUrl } from "@/lib/docs";
import { DocsLink } from "@/components/docs-link";
type Section = "installed" | "browse" | "updates";
type Section = "installed" | "browse" | "updates" | "registries";
const GRID_SIZES = ["auto", "2", "3", "4", "5", "6"] as const;
type GridSize = (typeof GRID_SIZES)[number];
@@ -146,6 +149,11 @@ function PluginsTabBody() {
icon: <Download className="size-3" />,
count: updates.filter((p) => !p.pinnedVersion).length || undefined,
},
{
id: "registries",
label: t("plugins.manager.tabs.registries"),
icon: <Library className="size-3" />,
},
];
return (
@@ -217,12 +225,14 @@ function PluginsTabBody() {
layout={layout}
layoutControls={layoutControls}
/>
) : (
) : section === "updates" ? (
<UpdatesSection
plugins={updates}
manager={manager}
onOpen={setDetailId}
/>
) : (
<RegistriesSection manager={manager} />
)}
</div>
</PanelShell>
@@ -442,6 +452,7 @@ function NameCell({ plugin, muted }: { plugin: PluginEntry; muted?: boolean }) {
{plugin.name}
</span>
{plugin.unverified && <UnverifiedBadge />}
<SourceBadge source={plugin.source} registryName={plugin.registryName} />
{plugin.isBeta && <BetaBadge />}
</div>
);
@@ -483,7 +494,10 @@ function InstalledSection({
sort,
);
const running = rows.filter((p) => p.status === "running");
const stopped = rows.filter((p) => p.status !== "running");
const onServer = rows.filter((p) => p.status === "server");
const stopped = rows.filter(
(p) => p.status !== "running" && p.status !== "server",
);
const pending = plugins.filter((p) => p.updateAvailable && !p.pinnedVersion);
const listColumns: DataColumn<PluginEntry>[] = [
@@ -587,6 +601,14 @@ function InstalledSection({
{collection(running)}
</Group>
)}
{onServer.length > 0 && (
<Group
title={t("plugins.manager.groups.onServer")}
count={onServer.length}
>
{collection(onServer)}
</Group>
)}
{stopped.length > 0 && (
<Group
title={t("plugins.manager.groups.notRunning")}
@@ -693,6 +715,10 @@ function InstalledCard({
facts={
<>
{plugin.unverified && <UnverifiedBadge />}
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
{plugin.isBeta && <BetaBadge />}
{plugin.author && <span>{plugin.author}</span>}
<span>v{plugin.version}</span>
@@ -717,7 +743,9 @@ function InstalledCard({
<div className="flex items-center gap-2 border-t border-border px-3 py-2">
<span className="size-1.5 shrink-0 bg-muted-foreground/40" />
<span className="text-[11px] text-muted-foreground">
{t("plugins.manager.status.disabled")}
{plugin.status === "server"
? t("plugins.manager.status.server")
: t("plugins.manager.status.disabled")}
</span>
</div>
) : (
@@ -806,11 +834,13 @@ function StatusCell({ plugin }: { plugin: PluginEntry }) {
? t("plugins.manager.status.blocked")
: failed
? t("plugins.manager.status.failed")
: !running
? t("plugins.manager.status.disabled")
: plugin.pinnedVersion
? t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion })
: t("plugins.manager.status.running");
: plugin.status === "server"
? t("plugins.manager.status.server")
: !running
? t("plugins.manager.status.disabled")
: plugin.pinnedVersion
? t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion })
: t("plugins.manager.status.running");
return (
<span
className="flex min-w-0 items-center gap-1.5 text-muted-foreground"
@@ -920,18 +950,34 @@ function BrowseSection({
}) {
const { t } = useTranslation();
const [category, setCategory] = useState<string | null>(null);
const [registry, setRegistry] = useState<string | null>(null);
const [sort, setSort] = useState<SortKey>("popular");
const [installedChoice, setInstalledChoice] = useStoredChoice(
"pluginsTab.browseInstalled",
["hide", "show"],
"hide",
);
const showInstalled = installedChoice === "show";
const categories = useMemo(() => categoriesOf(plugins), [plugins]);
const registries = manager.registries;
const registryFilter = registries.some((entry) => entry.id === registry)
? registry
: null;
const rows = sortPlugins(
plugins.filter(
(p) =>
(showInstalled || !p.installed) &&
matchesQuery(p, query) &&
(category === null || p.category === category),
(category === null || p.category === category) &&
(registryFilter === null || p.registryId === registryFilter),
),
sort,
);
const allInstalledHidden =
!showInstalled && plugins.length > 0 && plugins.every((p) => p.installed);
const listColumns: DataColumn<PluginEntry>[] = [
{
key: "name",
@@ -956,16 +1002,13 @@ function BrowseSection({
),
},
{
key: "installs",
header: t("plugins.manager.table.installs"),
key: "downloads",
header: t("plugins.manager.table.downloads"),
width: "110px",
hideBelow: "lg",
cell: (p) => (
<span className="text-muted-foreground">
<InstallCountFact
count={p.installCount}
source={p.installCountSource}
/>
<DownloadCount count={p.downloads} />
</span>
),
},
@@ -996,6 +1039,44 @@ function BrowseSection({
placeholder={t("plugins.manager.searchPlugins")}
>
<div className="ml-auto flex items-center gap-2">
<label className="flex items-center gap-1.5">
<span className="text-xs text-muted-foreground">
{t("plugins.manager.showInstalled")}
</span>
<FakeSwitch
checked={showInstalled}
onChange={(v) => setInstalledChoice(v ? "show" : "hide")}
/>
</label>
{registries.length > 1 && (
<Select
value={registryFilter ?? "__all"}
onValueChange={(v) => setRegistry(v === "__all" ? null : v)}
>
<SelectTrigger
size="sm"
className="w-auto gap-1.5 text-xs"
aria-label={t("plugins.manager.registries.filter")}
>
<Library className="size-3.5 text-muted-foreground" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all" className="text-xs">
{t("plugins.manager.registries.allRegistries")}
</SelectItem>
{registries.map((entry) => (
<SelectItem
key={entry.id}
value={entry.id}
className="text-xs"
>
{entry.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<SortSelect value={sort} onChange={setSort} />
{layoutControls}
</div>
@@ -1022,15 +1103,23 @@ function BrowseSection({
)}
{rows.length === 0 ? (
<EmptyState
icon={Puzzle}
title={t("plugins.manager.nothingHere")}
hint={
plugins.length === 0
? undefined
: t("plugins.manager.nothingHereHint")
}
/>
allInstalledHidden ? (
<EmptyState
icon={Puzzle}
title={t("plugins.manager.allInstalled")}
hint={t("plugins.manager.allInstalledHint")}
/>
) : (
<EmptyState
icon={Puzzle}
title={t("plugins.manager.nothingHere")}
hint={
plugins.length === 0
? undefined
: t("plugins.manager.nothingHereHint")
}
/>
)
) : (
<PluginCollection
plugins={rows}
@@ -1048,7 +1137,7 @@ function BrowseSection({
)}
<p className="text-[11px] text-muted-foreground">
{t("plugins.manager.communitySoon")}{" "}
{t("plugins.manager.registries.browseHint")}{" "}
<DocsLink core="communityRegistry" className="text-[11px]">
{t("plugins.manager.communitySubmit")}
</DocsLink>
@@ -1076,13 +1165,14 @@ function BrowseCard({
onOpen={onOpen}
facts={
<>
<SourceBadge
source={plugin.source}
registryName={plugin.registryName}
/>
{plugin.author && <span>{plugin.author}</span>}
{plugin.latestVersion && <span>v{plugin.latestVersion}</span>}
{plugin.category && <span>{plugin.category}</span>}
<InstallCountFact
count={plugin.installCount}
source={plugin.installCountSource}
/>
<DownloadCount count={plugin.downloads} />
</>
}
/>