feat: harden plugin api for separate repos, signed updates and generic external login

This commit is contained in:
LukeGus committed 2026-09-29 18:11:59 -05:00
1 parent fb9c168b6b
commit 80944301dd
310 files changed
+6577 -4927

No files matched your search

+1 -1
View File
@@ -116,7 +116,7 @@ export interface SessionGuestsV1 {
recordJoin: (shareId: string) => Promise<void>;
}
export const SESSION_GUESTS_KEY = "sessions.sharing.guests";
export const SESSION_GUESTS_KEY = "session-sharing.guests";
export interface RecordingSink {
/** Appends one batch; the first batch starts with the asciicast header. */
@@ -1,3 +1,5 @@
// The terminal surfaces need xterm's own stylesheet.
import "@xterm/xterm/css/xterm.css";
import { lazy, Suspense, type ComponentType } from "react";
import {
Braces,
@@ -0,0 +1,30 @@
import { useSyncExternalStore } from "react";
/**
* The terminal theme the host editor is previewing, so an open terminal for
* that host can show it before the host is saved. Null when nothing is being
* previewed.
*/
let preview: string | null = null;
const listeners = new Set<() => void>();
export function setThemePreview(theme: string | null): void {
if (theme === preview) return;
preview = theme;
for (const listener of listeners) listener();
}
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
export function useThemePreview(): string | null {
return useSyncExternalStore(
subscribe,
() => preview,
() => preview,
);
}
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { useMemo, useState, useEffect } from "react";
import { setThemePreview } from "../look/theme-preview";
import { Info, Palette, X, Zap } from "lucide-react";
import { toast } from "sonner";
import {
@@ -13,7 +14,6 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
useTabsSafe,
} from "@termix/plugin-sdk/ui";
import {
useSettings,
@@ -69,7 +69,8 @@ export function HostTerminalSettings({
const { t } = useTranslation();
const client = useTerminalClientSettings();
const userSettings = useSettings("user");
const { setPreviewTerminalTheme } = useTabsSafe();
// The preview ends when the editor section goes away.
useEffect(() => () => setThemePreview(null), []);
const stored = ((editorForm?.pluginSettings as HostPluginSettings)?.[
PLUGIN_ID
@@ -222,7 +223,7 @@ export function HostTerminalSettings({
onChange={(e) => {
const newTheme = e.target.value;
setField("theme", newTheme);
setPreviewTerminalTheme(newTheme);
setThemePreview(newTheme);
if (newTheme === "custom" && !form.customThemeColors) {
setField("customThemeColors", {
...TERMINAL_THEMES.termixDark.colors,
@@ -119,6 +119,7 @@ import {
isElectron,
} from "@termix/plugin-sdk/ui";
import {
notifyHostsChanged,
useTranslation,
invokeAction,
usePluginApi,
@@ -1558,9 +1559,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
onClick: () => {
void enableHostAutoTmux(api, hostConfig.id as number)
.then(() => {
window.dispatchEvent(
new CustomEvent("termix:hosts-changed"),
);
notifyHostsChanged();
toast.success(
t("terminal.autoTmuxEnabled", { host: hostLabel }),
);
@@ -1,7 +1,8 @@
import { lazy, Suspense } from "react";
import { useThemePreview } from "../look/theme-preview";
import { TerminalSquare } from "lucide-react";
import type { TerminalHandle, TerminalHostConfig } from "./Terminal";
import { useIsMobile, useTabsSafe } from "@termix/plugin-sdk/ui";
import { useIsMobile } from "@termix/plugin-sdk/ui";
import {
useTranslation,
invokeAction,
@@ -63,7 +64,7 @@ export function TerminalTabContent({
const tab = tabRecord as unknown as TerminalTabRecord;
const host = hostRecord as unknown as Host | undefined;
const { t } = useTranslation();
const { previewTerminalTheme } = useTabsSafe();
const previewTerminalTheme = useThemePreview();
const isMobile = useIsMobile();
const useSshTitle = readHostTerminalSettings(
hostTerminalValues(host),
@@ -48,6 +48,7 @@ import {
useIsMobile,
} from "@termix/plugin-sdk/ui";
import {
usePluginUiPreferences,
useHostActions,
useTabs,
useTranslation,
@@ -57,8 +58,6 @@ import {
} from "@termix/plugin-sdk/frontend";
type SelectedToolbarDensity = ToolbarDensity;
const DENSITY_STORAGE_KEY = "termix-terminal-toolbar-density";
/** Plugins contribute toolbar buttons here. Declared by the ssh-terminal plugin. */
const TOOLBAR_SLOT_ID = TERMINAL_TOOLBAR_SLOT;
@@ -69,16 +68,12 @@ const DENSITY_OPTIONS: { value: ToolbarDensity }[] = [
];
const DENSITIES = DENSITY_OPTIONS.map((option) => option.value);
function readStoredDensity(): ToolbarDensity {
if (typeof window === "undefined") return "labeled";
try {
const stored = window.localStorage.getItem(DENSITY_STORAGE_KEY);
return DENSITIES.includes(stored as ToolbarDensity)
? (stored as ToolbarDensity)
: "labeled";
} catch {
return "labeled";
}
const LEGACY_DENSITY_KEY = "termix-terminal-toolbar-density";
function asDensity(value: unknown): ToolbarDensity {
return DENSITIES.includes(value as ToolbarDensity)
? (value as ToolbarDensity)
: "labeled";
}
const CONTROL =
@@ -151,8 +146,28 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
const settings = useMemo(() => readToolbarSettings(host), [host]);
const { anchor } = settings;
const rememberDensity = settings.density === null;
// The remembered density is this plugin's UI preference, so an interface
// preset sets it too.
const { values: uiPrefs, set: setUiPref } = usePluginUiPreferences<{
toolbarDensity: ToolbarDensity;
}>();
const preferredDensity = asDensity(uiPrefs.toolbarDensity);
useEffect(() => {
// Before 2.9 the density lived in this browser's localStorage.
try {
const legacy = window.localStorage.getItem(LEGACY_DENSITY_KEY);
if (legacy === null) return;
window.localStorage.removeItem(LEGACY_DENSITY_KEY);
if (DENSITIES.includes(legacy as ToolbarDensity)) {
setUiPref("toolbarDensity", legacy as ToolbarDensity);
}
} catch {
// Storage can be unavailable in hardened browser contexts.
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [density, setDensity] = useState<SelectedToolbarDensity>(
() => settings.density ?? readStoredDensity(),
() => settings.density ?? preferredDensity,
);
const [responsiveDensity, setResponsiveDensity] =
useState<SelectedToolbarDensity>(() => density);
@@ -217,26 +232,12 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
setPosition(next);
}, [anchor]);
// A preset (or another tab) changing the saved density applies here too.
useEffect(() => {
if (!rememberDensity) return;
if (desktopViewportReady !== true || isMobile !== false) return;
try {
window.localStorage.setItem(DENSITY_STORAGE_KEY, density);
} catch {
// Storage can be unavailable in hardened browser contexts.
}
}, [density, desktopViewportReady, isMobile, rememberDensity]);
// Picking an interface preset seeds this key, so pick the new value up
// without needing the tab to remount.
useEffect(() => {
if (!rememberDensity) return;
if (desktopViewportReady !== true || isMobile !== false) return;
const handler = () => setDensity(readStoredDensity());
window.addEventListener("terminalToolbarDensityChanged", handler);
return () =>
window.removeEventListener("terminalToolbarDensityChanged", handler);
}, [desktopViewportReady, isMobile, rememberDensity]);
setDensity(preferredDensity);
}, [preferredDensity, desktopViewportReady, isMobile, rememberDensity]);
useEffect(() => {
if (desktopViewportReady !== true || isMobile !== false) return;
const measurement = measurementRef.current;
@@ -375,6 +376,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
const setAndStoreDensity = (next: ToolbarDensity) => {
setDensity(next);
if (rememberDensity) setUiPref("toolbarDensity", next);
};
const reportImageResult = (
action: () => void | Promise<void>,
@@ -692,7 +694,10 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
}}
>
{collapsed ? (
<div className="terminal-toolbar-collapsed-shell flex rounded-sm border border-border bg-background/90 p-0.5 shadow-lg backdrop-blur-sm">
<div
data-toolbar-collapsed
className="relative flex rounded-sm border border-border bg-background/90 p-0.5 shadow-lg backdrop-blur-sm"
>
{expandButton}
{grabButton()}
</div>