feat: load plugin frontends at runtime and remove plugin ids from the shell

This commit is contained in:
LukeGus committed 2026-09-22 20:15:52 -05:00
1 parent 49f0286a14
commit e88d5a52e3
721 files changed
+54265 -44416

No files matched your search

@@ -1,6 +1,6 @@
import { getErrorMessage } from "@/lib/error-message.js";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { toast } from "sonner";
import {
ArrowLeft,
@@ -35,7 +35,8 @@ import {
import { NotificationChannelDialog } from "@/sidebar/NotificationChannelDialog";
import { getSnippets } from "@/api/snippets-api";
import { getSSHHosts } from "@/api/ssh-host-management-api";
import { listFleets } from "@/api/fleets-api";
import { invokeAction } from "@termix/plugin-sdk/frontend";
import {
AutomationEditor,
emptyDraft,
@@ -70,6 +71,14 @@ function timeAgo(iso: string | null): string {
return `${Math.floor(hr / 24)}d ago`;
}
/** Fleets from the fleets plugin, or none when it is not running. */
async function listFleets(): Promise<{ id: number; name: string }[]> {
const result = await invokeAction("fleets.list");
return Array.isArray(result)
? (result as { id: number; name: string }[])
: [];
}
export function AutomationsPanel({
active = true,
onEditingChange,
@@ -1,6 +1,6 @@
import { getErrorMessage } from "@/lib/error-message.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Plus } from "lucide-react";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
@@ -1,4 +1,4 @@
import { useTranslation } from "react-i18next";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Label } from "@/components/label";
import {
Select,
@@ -1,4 +1,4 @@
import { useTranslation } from "react-i18next";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { ChevronDown, ChevronUp, Trash2 } from "lucide-react";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
@@ -1,4 +1,4 @@
import { useTranslation } from "react-i18next";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Input } from "@/components/input";
import { Label } from "@/components/label";
import {
@@ -1,36 +0,0 @@
/**
* Frontend half of the automations plugin.
*
* AutomationsPanel is a left-sidebar railView panel (opened from the rail,
* not a Tab), the same shape as FleetsPanel/HostsPanel/AiPanel. None of those
* go through registerRailItem/registerTabComponent either -- per
* src/ui/shell/pluginLoader.ts's own comment, a rail-view panel like this is
* rendered directly by AppShell.tsx rather than registered at runtime, the
* same way ai's AiPanel is. AutomationsPanel is reached today through a
* direct relative import in AppShell.tsx
* (../../plugins/automations/frontend/AutomationsPanel) and in
* src/ui/shell/tabUtils.tsx (for its promotable/dockable tab surface), not
* through register() below.
*
* register()/unregister() still follow the same shape every other plugin's
* frontend entry does, so this is ready the moment a real frontend plugin
* loader exists -- see plugins/ssh-terminal/frontend/index.mjs and
* src/ui/tests/sidebar/plugin-extension-seam.test.tsx, which says outright
* "there is no plugin loader yet".
*/
export const id = "automations";
export const railViewId = "automations";
export async function register({ registerRailItem, icons }) {
registerRailItem?.({
id: railViewId,
icon: icons.Workflow,
labelKey: "nav.automations",
separatorAfter: true,
});
}
export async function unregister({ unregisterRailItem }) {
unregisterRailItem?.(railViewId);
}
@@ -0,0 +1,51 @@
import { Workflow } from "lucide-react";
import type {
PanelProps,
TabProps,
TermixApp,
} from "@termix/plugin-sdk/frontend";
import { AutomationsPanel } from "./AutomationsPanel";
import { listAutomations } from "./automations-api";
const VIEW_ID = "automations";
function Panel({ active, setEditing }: PanelProps) {
return (
<div className="flex-1 min-h-0 overflow-y-auto">
<AutomationsPanel active={active} onEditingChange={setEditing} />
</div>
);
}
function AutomationsTab({ isVisible }: TabProps) {
return <AutomationsPanel active={isVisible} />;
}
export function activate(app: TermixApp): void {
app.registerRailItem({
id: VIEW_ID,
icon: Workflow,
titleKey: "nav.automations",
promotable: true,
after: "macros",
order: 30,
});
app.registerPanel(VIEW_ID, Panel);
app.registerTab(VIEW_ID, AutomationsTab, {
icon: Workflow,
titleKey: "nav.automations",
singleton: true,
hostless: true,
panelFrame: true,
});
// The assistant's @-mentions list automations through this.
app.registerAction("automations.list", () => listAutomations());
app.registerSlotContribution("onboarding.features", {
actionId: "automations.feature",
titleKey: "onboarding.feature_automations",
descriptionKey: "onboarding.feature_automations_desc",
icon: Workflow,
});
}