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

@@ -46,6 +46,7 @@ import {
} from "@termix/plugin-sdk/ui";
import { ConnectionLogPanel } from "@termix/plugin-sdk/ui";
import {
usePluginUiPreferences,
invokeAction,
useConnectionRetry,
logActivity,
@@ -177,25 +178,32 @@ function FileManagerContent({
const [isReconnecting, setIsReconnecting] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState("");
const [lastRefreshTime, setLastRefreshTime] = useState<number>(0);
const [viewMode, setViewMode] = useState<"grid" | "list">(() => {
const saved = localStorage.getItem("fileManagerViewMode");
return saved === "grid" || saved === "list" ? saved : "grid";
});
// An interface preset sets this too, so it is a plugin UI preference.
const { values: fmPrefs, set: setFmPref } = usePluginUiPreferences<{
viewMode: "grid" | "list";
}>();
const viewMode: "grid" | "list" =
fmPrefs.viewMode === "list" ? "list" : "grid";
const setViewMode = useCallback(
(next: "grid" | "list") => setFmPref("viewMode", next),
[setFmPref],
);
useEffect(() => {
// Before 2.9 the view mode lived in this browser's localStorage.
try {
const legacy = localStorage.getItem("fileManagerViewMode");
if (legacy === null) return;
localStorage.removeItem("fileManagerViewMode");
if (legacy === "grid" || legacy === "list") setFmPref("viewMode", legacy);
} catch {
// Storage can be unavailable.
}
}, []);
const [density, setDensity] = useState<"comfortable" | "compact">(() =>
localStorage.getItem("fileManagerDensity") === "compact"
? "compact"
: "comfortable",
);
// Picking an interface preset seeds this key from another part of the app.
useEffect(() => {
const handler = () => {
const saved = localStorage.getItem("fileManagerViewMode");
if (saved === "grid" || saved === "list") setViewMode(saved);
};
window.addEventListener("fileManagerViewModeChanged", handler);
return () =>
window.removeEventListener("fileManagerViewModeChanged", handler);
}, []);
const [sortBy, setSortBy] = useState<"name" | "modified" | "size">(() => {
const saved = localStorage.getItem("fileManagerSortBy");
return saved === "name" || saved === "modified" || saved === "size"
@@ -3328,10 +3336,6 @@ function FileManagerContent({
}
}, [currentHost?.id]);
useEffect(() => {
localStorage.setItem("fileManagerViewMode", viewMode);
}, [viewMode]);
useEffect(() => {
localStorage.setItem("fileManagerDensity", density);
}, [density]);
@@ -15,7 +15,7 @@ import {
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { Kbd, KbdKey, KbdSeparator } from "@termix/plugin-sdk/ui";
import type { LocalFileEntry } from "@termix/plugin-sdk/ui";
import type { LocalFileEntry } from "@termix/plugin-sdk/frontend";
const VIEWPORT_PADDING = 16;
@@ -28,7 +28,7 @@ import {
} from "lucide-react";
import { cn } from "@termix/plugin-sdk/ui";
import { Button, Input } from "@termix/plugin-sdk/ui";
import type { LocalFileEntry } from "@termix/plugin-sdk/ui";
import type { LocalFileEntry } from "@termix/plugin-sdk/frontend";
import {
createLocalFile,
createLocalFolder,
@@ -9,10 +9,11 @@
// configured Remote Sync server) and the main process resolves the rest, so a
// compromised renderer cannot turn the transfer bridge into an HTTP client.
import type {
LocalTransferOrigin,
LocalTransferProgress,
} from "@termix/plugin-sdk/ui";
import {
getLocalAuthToken,
type LocalTransferOrigin,
type LocalTransferProgress,
} from "@termix/plugin-sdk/frontend";
import { getSessionOrigin } from "./client";
import { getDeviceId } from "@termix/plugin-sdk/ui";
@@ -67,11 +68,7 @@ function transferOriginFor(sessionId: string): LocalTransferOrigin {
*/
function localAuthTokenFor(origin: LocalTransferOrigin): string | undefined {
if (origin !== "local") return undefined;
try {
return localStorage.getItem("jwt") || undefined;
} catch {
return undefined;
}
return getLocalAuthToken() ?? undefined;
}
export function createLocalTransferId(prefix: string): string {
@@ -3,7 +3,7 @@ import { createElement } from "react";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import type { FileItem } from "../host-types";
import type { LocalWalkResult } from "@termix/plugin-sdk/ui";
import type { LocalWalkResult } from "@termix/plugin-sdk/frontend";
import { useSettings } from "@termix/plugin-sdk/frontend";
import {
cancelLocalTransfer,
+22 -3
View File
@@ -1,6 +1,6 @@
import { fileManagerHostSetting } from "./host-settings";
import type { ComponentType } from "react";
import { FolderSearch, ArrowLeftRight } from "lucide-react";
import { FolderOpen, FolderSearch, ArrowLeftRight } from "lucide-react";
import {
invokeAction,
type PluginHostRecord,
@@ -70,8 +70,14 @@ function openEditorAction(
app.tabs.openTab(host, "files", { data: { initialFilePath: filePath } });
}
/** Where this plugin serves the streaming routes the desktop app calls. */
const TRANSFER_API_PATH = "/plugin-api/file-manager";
export function activate(app: TermixApp): void {
setFileManagerApp(app);
if (typeof window !== "undefined") {
void window.electronAPI?.localTransfer?.setApiPath?.(TRANSFER_API_PATH);
}
app.onDispose(() => setFileManagerApp(null));
app.registerTab("files", FilesTab as unknown as ComponentType<TabProps>, {
icon: FolderSearch,
@@ -135,8 +141,21 @@ export function activate(app: TermixApp): void {
const openHost = ((host: PluginHostRecord | null, path?: string) =>
openHostAction(app, host, path)) as never;
app.registerAction("files.openHost", openHost);
// The shell's "open in file manager" tab button asks for this.
app.registerAction("host.openFiles", openHost);
// "Open File Manager" in the tab bar's menu, for a terminal tab. The
// terminal answers with its working directory and opens the files tab.
app.registerAction("file-manager.openFromTab", ((handle: unknown) =>
(
handle as { openFileManager?: () => void } | null
)?.openFileManager?.()) as never);
app.registerSlotContribution("tab.menu", {
actionId: "file-manager.openFromTab",
titleKey: "nav.openFileManager",
icon: FolderOpen,
kind: "button",
when: (context) =>
typeof (context.handle as { openFileManager?: unknown } | null)
?.openFileManager === "function",
});
app.registerAction("files.openEditor", ((
host: PluginHostRecord | null,
filePath: string,
@@ -8,7 +8,7 @@ import type {
LocalFsResult,
LocalTrashResult,
LocalWalkResult,
} from "@termix/plugin-sdk/ui";
} from "@termix/plugin-sdk/frontend";
import { isElectron } from "@termix/plugin-sdk/ui";
export function isLocalFileBrowserAvailable(): boolean {
@@ -1,7 +1,7 @@
// Pure helpers shared by the dual-pane (local <-> remote) file manager code.
// Kept free of React/DOM so they can be unit tested directly.
import type { LocalFileEntry } from "@termix/plugin-sdk/ui";
import type { LocalFileEntry } from "@termix/plugin-sdk/frontend";
/** Custom MIME type carried by drags that originate in the local pane. */
export const LOCAL_FILES_DRAG_MIME = "application/x-termix-local-files";