mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 15:15:24 +00:00
v26.10.1 (#1599)
* 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:
174 files changed
+94334
-39995
No files matched your search
+121
-31
@@ -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} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user