* feat: shell controller, TabStage, layout and theme plugins with repair view

* feat: move the classic layout into its own bundled plugin

* feat: move color themes into a bundled themes plugin

* fix: plugin tests and type checks work with a linked sdk

* fix: home tab, layout switch and missing layout fallbacks for looks

* chore: add classic layout and themes to the readme

* chore: drop example plugin links from the plugin guide

* feat: plugin filters by type, paged browse, hide plugins needing newer termix

* feat: move desktop, status, import, audit and branding features into plugins

* feat: host lock api so a plugin can load hosts from a config file (#1581)

* chore: add hosts config to the plugin table

* feat: login policy hook for plugins and optional https redirect (#1584)

* chore: add login security to the plugin table

* feat: api key socket tickets so a master can open remote sessions (#1654)

* feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654)

* chore: add sync to the plugin table

* feat: move the sync screens and wizard out of core into the plugin (#1580)

* fix: add a neutral linked desktop hook for plugins (#1580)

* fix: show the linked account's name and admin flag in the app (#1580)

* feat: ssh transport providers, core ssh relay and plugin folders api

* feat: support FIDO2 security keys through ssh agent

* feat: add companion panels beside host tabs

* feat: mTLS client certificates for desktop server links

* feat: native web view for plugins in the desktop app

* fix: detect the desktop link window by Electron's default user agent

Fixes #1675

* fix: changelog for desktop link fix

* fix: keep text primary keys in the plugin data export

* fix: ship sharp binaries for Windows arm64 builds

* feat: client cert on the sync link types (sdk 1.0.18)

* chore: update translations

* feat: themed title bar for the desktop app (#1690)

* chore: changelog for the themed title bar

* fix: draw the title bar in the sidebar color

* feat: per host layout for the terminal panel bar

* chore: sdk 1.0.19 for the panel bar layout

* chore: bundle all 43 plugins for 26.10.3

* chore: update translations

---------

Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com>
Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-20 16:03:29 -05:00
1 parent aff29a9581
commit a89ccc6b07
577 files changed
+76479 -101831

No files matched your search

@@ -1,82 +0,0 @@
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import {
AccentPicker,
FontSizePicker,
LanguageSelect,
ThemeGrid,
UiFontSelect,
useAppearanceSettings,
} from "@/settings/appearance/appearance-controls";
function Field({
label,
hint,
children,
}: {
label: string;
hint?: string;
children: ReactNode;
}) {
return (
<div className="flex flex-col gap-1.5">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{label}
</span>
{children}
{hint && (
<span className="text-[10px] text-muted-foreground">{hint}</span>
)}
</div>
);
}
/** Every change applies right away, so the page itself is the preview. */
export function AppearanceStep() {
const { t } = useTranslation();
const appearance = useAppearanceSettings();
return (
<div className="flex flex-col gap-5">
<p className="text-xs text-muted-foreground">
{t("onboarding.appearanceIntro")}
</p>
<Field label={t("newUi.sidebar.userProfile.themeLabel")}>
<ThemeGrid value={appearance.theme} onChange={appearance.setTheme} />
</Field>
<Field label={t("newUi.sidebar.userProfile.accentColorLabel")}>
<AccentPicker
value={appearance.accent}
onChange={appearance.setAccent}
/>
</Field>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<Field
label={t("newUi.sidebar.userProfile.interfaceFontLabel")}
hint={t("newUi.sidebar.userProfile.interfaceFontDescription")}
>
<UiFontSelect
value={appearance.uiFont}
onChange={appearance.setUiFont}
/>
</Field>
<Field label={t("newUi.sidebar.userProfile.fontSizeLabel")}>
<FontSizePicker
value={appearance.fontSize}
onChange={appearance.setFontSize}
/>
</Field>
</div>
<Field label={t("newUi.sidebar.userProfile.languageLabel")}>
<LanguageSelect
value={appearance.language}
onChange={appearance.setLanguage}
/>
</Field>
</div>
);
}
@@ -1,79 +0,0 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Cloud, Laptop } from "lucide-react";
import { LinkServerDialog } from "@/settings/sync/LinkServerDialog";
import { useSyncStatus } from "@/hooks/use-sync-status";
import { setSyncStatus } from "@/lib/sync-status";
import { notifySyncChanged } from "@/lib/linked-server";
/**
* Desktop only: use Termix on this device alone, or link it to a server.
* Either choice can be changed later from Sync.
*/
export function DesktopSyncStep() {
const { t } = useTranslation();
const status = useSyncStatus();
const [linkOpen, setLinkOpen] = useState(false);
const linked = !!status?.linked;
const options = [
{
id: "local",
icon: Laptop,
title: t("onboarding.desktopLocalTitle"),
body: t("onboarding.desktopLocalBody"),
selected: !linked,
onClick: () => {},
},
{
id: "link",
icon: Cloud,
title: t("onboarding.desktopLinkTitle"),
body: linked
? t("onboarding.desktopLinked", {
server: status?.serverName || status?.serverUrl,
})
: t("onboarding.desktopLinkBody"),
selected: linked,
onClick: () => !linked && setLinkOpen(true),
},
];
return (
<div className="flex flex-col gap-3">
<p className="text-xs text-muted-foreground">
{t("onboarding.desktopBody")}
</p>
<div className="flex flex-col gap-1.5">
{options.map(({ id, icon: Icon, title, body, selected, onClick }) => (
<button
key={id}
type="button"
onClick={onClick}
className={`flex items-start gap-2.5 border p-2.5 text-left transition-colors ${
selected
? "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>
<LinkServerDialog
open={linkOpen}
onOpenChange={setLinkOpen}
onLinked={(next) => {
setSyncStatus(next);
notifySyncChanged();
}}
/>
</div>
);
}
-109
View File
@@ -1,109 +0,0 @@
import { useTranslation } from "react-i18next";
import { Gauge, Sparkles, Wrench } from "lucide-react";
import { useUiPreferencesContext } from "@/contexts/UiPreferencesContext";
import { applyPresetSideEffects } from "@/lib/apply-ui-preset";
import { PRESETS, type UiPreset } from "@/types/ui-preferences";
const CHOICES: Exclude<UiPreset, "custom">[] = [
"simple",
"balanced",
"advanced",
];
const ICONS = { simple: Sparkles, balanced: Gauge, advanced: Wrench } as const;
/**
* A miniature of the host list at the given preset. Deliberately drawn rather
* than rendering the real component: the point is to show the shape difference
* (how many rows fit, how much detail each carries) at a glance.
*/
function HostRowPreview({ preset }: { preset: Exclude<UiPreset, "custom"> }) {
const { hostList } = PRESETS[preset];
const rows = hostList.density === "compact" ? 5 : 3;
return (
<div className="flex flex-col gap-[3px] border border-border/60 bg-background/60 p-1.5">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex items-stretch gap-1">
{hostList.showStatusStripes && (
<div className="w-[2px] shrink-0 bg-accent-brand/70" />
)}
<div className="flex flex-1 flex-col gap-[2px] py-[2px]">
<div className="h-[3px] w-2/3 bg-muted-foreground/40" />
{hostList.density === "comfortable" && (
<div className="h-[2px] w-1/2 bg-muted-foreground/20" />
)}
{hostList.showTags && (
<div className="flex gap-[2px]">
<div className="h-[3px] w-3 bg-muted-foreground/20" />
<div className="h-[3px] w-2 bg-muted-foreground/20" />
</div>
)}
{hostList.showResourceBars && (
<div className="flex gap-[2px]">
<div className="h-[2px] w-4 bg-accent-brand/40" />
<div className="h-[2px] w-3 bg-accent-brand/25" />
</div>
)}
</div>
</div>
))}
</div>
);
}
export function PresetStep() {
const { t } = useTranslation();
const ctx = useUiPreferencesContext();
const selected = ctx?.preferences.preset ?? "balanced";
return (
<div className="flex flex-col gap-3">
<p className="text-xs text-muted-foreground">
{t("onboarding.presetIntro")}
</p>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
{CHOICES.map((preset) => {
const Icon = ICONS[preset];
const active = selected === preset;
return (
<button
key={preset}
type="button"
aria-pressed={active}
onClick={() => {
ctx?.setPreset(preset);
ctx?.clearAllOverrides();
void applyPresetSideEffects(preset);
}}
className={`flex flex-col gap-2 border p-2.5 text-left transition-colors ${
active
? "border-accent-brand bg-accent-brand/10"
: "border-border bg-card hover:bg-muted/40"
}`}
>
<span className="flex items-center gap-1.5 text-xs font-semibold">
<Icon
size={13}
className={
active ? "text-accent-brand" : "text-muted-foreground"
}
/>
{t(`newUi.sidebar.userProfile.preset_${preset}`)}
</span>
<HostRowPreview preset={preset} />
<span className="text-[10px] leading-snug text-muted-foreground">
{t(`newUi.sidebar.userProfile.preset_${preset}_desc`)}
</span>
</button>
);
})}
</div>
<p className="text-[10px] text-muted-foreground/70">
{t("onboarding.presetChangeLater")}
</p>
</div>
);
}