* 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

+256 -2
View File
@@ -248,6 +248,129 @@ export interface TabsApi extends Pick<
onReady: (listener: () => void) => Disposer;
}
// ---------------------------------------------------------------------------
// Layouts (plugin API 1.4)
// ---------------------------------------------------------------------------
/**
* A whole-app layout. Core renders the chosen one inside its providers and an
* error boundary; everything it shows comes from useShell(). Declare it in
* manifest contributes.layouts and add the ui:layout capability.
*/
export interface LayoutContribution {
id: string;
/** Your own i18n key or plain text, the same as the manifest's label. */
label: string;
component: ComponentType;
/** Shown on phones and narrow windows. The desktop app never uses it. */
mobileComponent?: ComponentType;
/** Theme to start with when nobody chose one. */
defaultTheme?: string;
}
/** A tab as a layout sees it. Splits are tabs too, with a `split` field. */
export interface ShellTabState extends PluginTabRecord {
instanceId: string;
customLabel?: string;
/** Set while the tab sits in a split pane. */
parentSplitTabId?: string;
openedAt?: number;
}
/** What a dock region shows and how it is sized, in Normal-size pixels. */
export interface ShellDockState {
view: string | null;
open: boolean;
width: number;
dragging: boolean;
setView: (view: string | null) => void;
setWidth: (width: number) => void;
/** Starts a drag from the dock's edge, from a mousedown. */
startResize: (event: { clientX: number; preventDefault: () => void }) => void;
resetWidth: () => void;
}
/** The live shell: state and actions, nothing drawn. Read with useShell(). */
export interface ShellController {
tabs: {
list: ShellTabState[];
/** What a tab bar shows: tabs not sitting in a split. */
topLevel: ShellTabState[];
activeId: string;
/** True once saved tabs were restored after login. */
ready: boolean;
/** The user turned on tab numbers. */
showNumbers: boolean;
setActive: (id: string) => void;
reorder: (topLevel: ShellTabState[]) => void;
close: (id: string) => Promise<void>;
rename: (id: string, label: string) => void;
/** Reconnects or reloads a session tab. */
refresh: (id: string) => void;
reconnectAll: () => void;
openSingleton: (type: string) => string | undefined;
};
split: {
/** The split shown now, or null. */
activeId: string | null;
focusedPaneId: string | null;
/** Every split with its panes, for a split menu. */
summaries: Array<{
id: string;
label: string;
full: boolean;
panes: Array<{
id: string;
index: number;
tabId: string | null;
label: string | null;
}>;
}>;
activeFull: boolean;
/** Split a tab, apply a preset, unsplit, add to a pane. */
handleAction: (action: Record<string, unknown> & { kind: string }) => void;
};
hosts: {
list: PluginHostRecord[];
loading: boolean;
loaded: boolean;
reload: () => Promise<void>;
};
regions: {
left: ShellDockState & { view: string };
right: ShellDockState;
/** What a rail click does: toggles or switches the left dock. */
railClick: (view: string) => void;
openInRightDock: (view: string) => void;
toggleRightDock: () => void;
};
palette: { isOpen: boolean; open: () => void; close: () => void };
fullscreen: { active: boolean; toggle: () => void };
/** The same callbacks tabs and panels get. */
actions: ShellApi;
env: {
isMobile: boolean;
isElectron: boolean;
user: { username: string; isAdmin: boolean };
permissions: string[];
branding: {
appName: string | null;
tagline: string | null;
logo: string | null;
};
};
/** Core registries, as hooks a layout calls while rendering. */
registries: {
useRailItems: () => ReadonlyArray<{ id: string } & Record<string, unknown>>;
usePanels: () => ReadonlyArray<{ id: string } & Record<string, unknown>>;
useTabTypes: () => ReadonlyArray<{ id: string } & Record<string, unknown>>;
useDashboardCards: () => ReadonlyArray<
{ id: string } & Record<string, unknown>
>;
};
logout: (options?: { manual?: boolean }) => void;
}
// ---------------------------------------------------------------------------
// Registrations
// ---------------------------------------------------------------------------
@@ -530,8 +653,16 @@ export interface HostBadgeContribution {
id: string;
when: (host: PluginHostRecord) => boolean;
component: ComponentType<{ host: PluginHostRecord }>;
/** Lower comes first. Ties sort by id. */
order?: number;
}
/** The dot or stripe that shows a host's status on its row. */
export type HostStatusIndicatorContribution = HostBadgeContribution;
/** A control in the host editor's toolbar. */
export type HostEditorToolbarItemContribution = HostBadgeContribution;
export interface HostContextMenuItemContribution {
id: string;
titleKey: string;
@@ -683,6 +814,66 @@ export interface SettingsComponentProps {
running: boolean;
}
/** What a companion starts with. Read from the plugin's own settings. */
export interface CompanionDefaults {
/** Open by itself the first time a matching tab shows. */
open?: boolean;
side?: "left" | "right" | "bottom";
/** Pixels of width, or height at the bottom. */
size?: number;
/** Show the edge button that opens and closes it. Default true. */
edgeButton?: boolean;
}
/** A panel a tab type can show beside it, for a picker. */
export interface CompanionInfo {
/** The id registerCompanionLayout's hidden and order lists use. */
id: string;
titleKey: string;
icon?: IconComponent;
}
/** How a host wants the panel bar beside a tab. */
export interface CompanionLayout {
/** False removes the bar and its panels. */
enabled?: boolean;
/** Where a panel opens until the user moves it. */
side?: "left" | "right" | "bottom";
/** Companion ids in bar order. */
order?: string[];
/** Companion ids left out. */
hidden?: string[];
}
export interface CompanionProps {
/** The tab it sits beside. */
tab: PluginTabRecord;
host?: PluginHostRecord;
/** The host in the shape the connection APIs take. */
sshHost?: Record<string, unknown>;
/** The tab's surface handle (see TabHandle), read at call time. */
getHandle: () => TabHandle | null;
side: "left" | "right" | "bottom";
/** False while the tab is in the background or the panel is closed. */
isVisible: boolean;
close: () => void;
setSide: (side: "left" | "right" | "bottom") => void;
}
export interface CompanionOptions {
icon?: IconComponent;
titleKey?: string;
order?: number;
/** Tab types it applies to. Any tab with a host when empty. */
tabTypes?: string[];
when?: (tab: PluginTabRecord, host?: PluginHostRecord) => boolean;
/**
* A hook returning the starting state. It runs in the plugin's scope, so it
* can read the plugin's settings.
*/
useDefaults?: () => CompanionDefaults;
}
export type ActionHandler = (...args: never[]) => unknown;
export interface ActionSlotDefinition {
@@ -715,10 +906,11 @@ export type OnboardingMode = "full" | "partial" | "rerun";
* Where a plugin step sits in onboarding. "setup" comes right after the
* appearance steps, for things that need doing before the feature works
* (like pointing at a guacd server). "explore" (the default) comes after
* that, "security" after the account security step. Onboarding is for
* that, "security" after the account security step. "start" comes before
* the appearance steps, for choices that shape how the rest looks. Onboarding is for
* choices, not for explaining features.
*/
export type OnboardingSection = "setup" | "explore" | "security";
export type OnboardingSection = "start" | "setup" | "explore" | "security";
export interface OnboardingStepProps {
/**
@@ -761,6 +953,16 @@ export interface OnboardingStepContribution {
permission?: string;
section?: OnboardingSection;
order?: number;
/**
* Exact place in the flow, 1 to 99, instead of a section. Core steps sit at
* 10 (plugins), 20, 25, 30 (preset), 40 (appearance) and 90 (security).
*/
position?: number;
/**
* Older step keys this step took over, like "preset". People who already
* saw one of them are not shown this step again.
*/
replaces?: string[];
when?: (context: {
isAdmin: boolean;
isDesktop: boolean;
@@ -926,12 +1128,40 @@ export interface TermixApp extends TermixAppInfo {
component: ComponentType<TabProps>,
options?: TabOptions,
) => Disposer;
/**
* A panel that sits beside any host tab the options match (a terminal, a
* desktop, ...), bound to that tab's host. The user opens, resizes and moves
* it; the tab keeps running underneath.
*/
registerCompanion: (
id: string,
component: ComponentType<CompanionProps>,
options?: CompanionOptions,
) => Disposer;
/**
* Lets a plugin decide, per host, how the panel bar looks on one tab type.
* Read from the host payload, so it must not call hooks.
*/
registerCompanionLayout: (
tabType: string,
resolve: (host: PluginHostRecord | undefined) => CompanionLayout,
) => Disposer;
/** Opens a companion on a tab, or closes it when it is showing. */
toggleCompanion: (tab: PluginTabRecord, id?: string) => void;
registerHostEditorSection: (
section: HostEditorSectionContribution,
) => Disposer;
registerHostAction: (action: HostActionContribution) => Disposer;
registerHostProtocol: (protocol: HostProtocolContribution) => Disposer;
registerHostBadge: (badge: HostBadgeContribution) => Disposer;
/** Draws a host row's status. A layout renders every registered one. */
registerHostStatusIndicator: (
indicator: HostStatusIndicatorContribution,
) => Disposer;
/** Adds a control to the host editor's toolbar. */
registerHostEditorToolbarItem: (
item: HostEditorToolbarItemContribution,
) => Disposer;
registerHostContextMenuItem: (
item: HostContextMenuItemContribution,
) => Disposer;
@@ -988,6 +1218,12 @@ export interface TermixApp extends TermixAppInfo {
registerSshAuthEditor: (editor: SshAuthEditorContribution) => Disposer;
registerLoginMethod: (method: LoginMethodContribution) => Disposer;
registerSecondFactorUI: (factor: SecondFactorContribution) => Disposer;
/**
* A whole-app layout. Declared in manifest contributes.layouts with the
* ui:layout capability. Only the chosen layout's plugin is loaded, so
* register it in activate. Needs plugin API 1.4.
*/
registerLayout: (layout: LayoutContribution) => Disposer;
/**
* Translates a key from the plugin's own locales, for code outside a
@@ -1122,6 +1358,7 @@ export interface PluginHostBridge {
useHosts: () => { hosts: PluginHostRecord[]; loaded: boolean };
useCurrentUser: () => CurrentUser | null;
useTheme: () => { theme: "light" | "dark" };
useShell: () => ShellController | null;
toast: ToastApi;
getApi: (pluginId: string) => PluginApiClient;
getApiFor: (pluginId: string, origin: unknown) => PluginApiClient;
@@ -1138,6 +1375,7 @@ export interface PluginHostBridge {
hostProtocols: (host: PluginHostRecord) => string[];
useHostStatus: (hostId: number | undefined) => HostStatusInfo | null;
useHostActions: () => HostActionContribution[];
useCompanions: (tabType: string) => CompanionInfo[];
useActivityTypes: () => string[];
activityTarget: (type: string) => ActivityTargetInfo | undefined;
useExtensions: (pointId: string) => ExtensionContribution[];
@@ -1372,6 +1610,14 @@ export function useTheme(): { theme: "light" | "dark" } {
return requireHost().useTheme();
}
/**
* The live shell, for a layout. Null outside the signed-in app. Needs plugin
* API 1.4.
*/
export function useShell(): ShellController | null {
return requireHost().useShell();
}
export function useToast(): ToastApi {
return requireHost().toast;
}
@@ -1539,6 +1785,14 @@ export function useHostActions(): HostActionContribution[] {
return requireHost().useHostActions();
}
/**
* Every panel that can open beside a tab of this type, for a settings screen
* that lets the user hide or reorder them. Reactive.
*/
export function useCompanions(tabType: string): CompanionInfo[] {
return requireHost().useCompanions(tabType);
}
/**
* Recent-activity types every registered tab claims (registerTab's
* activityTypes), core's and plugins'. For a filter that lists what a user