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

+91 -115
View File
@@ -14,22 +14,16 @@ import {
Server,
Settings,
Terminal,
FolderOpen,
FolderSearch,
Box,
Globe,
HardDrive,
Plus,
MessagesSquare,
LifeBuoy,
Search,
Activity,
Network,
User,
KeyRound,
Layers, // --- tmux-monitor ---
Monitor,
MousePointerClick,
Clock,
Folder,
Pencil,
@@ -43,10 +37,17 @@ import {
} from "@/main-axios";
import type { Host, TabType, Tab, Snippet } from "@/types/ui-types";
import { canEditHost } from "@/sidebar/host-permissions";
import { RAIL_ITEMS, RAIL_UTILITY_ITEMS } from "@/sidebar/rail-items";
import { useAiAvailability } from "@/hooks/use-ai-availability";
import { RAIL_UTILITY_ITEMS, useRailItems } from "@/sidebar/rail-items";
import { useSnippetRunner } from "@/hooks/use-snippet-runner.tsx";
import { isTabTypeAvailable } from "@/shell/pluginLoader";
import {
defaultConnectAction,
hostActionsFor,
runHostAction,
useHostActions,
} from "@/sidebar/host-contributions";
import { paletteEntriesFor, usePaletteEntries } from "./palette-registry";
import { activityTarget } from "@/lib/activity-types";
import { shell } from "@/plugin-host/shell-bridge";
interface CommandPaletteProps {
isOpen: boolean;
@@ -59,47 +60,18 @@ interface CommandPaletteProps {
onOpenPanel?: (view: string) => void;
}
const ACTIVITY_ICONS: Record<string, React.ReactNode> = {
terminal: <Terminal className="size-3.5" />,
file_manager: <FolderOpen className="size-3.5" />,
server_stats: <Activity className="size-3.5" />,
tunnel: <Network className="size-3.5" />,
docker: <Box className="size-3.5" />,
telnet: <MessagesSquare className="size-3.5" />,
vnc: <MousePointerClick className="size-3.5" />,
rdp: <Monitor className="size-3.5" />,
};
const ACTIVITY_TAB_TYPE: Record<string, TabType> = {
terminal: "terminal",
file_manager: "files",
server_stats: "host-metrics",
tunnel: "tunnel",
docker: "docker",
telnet: "telnet",
vnc: "vnc",
rdp: "rdp",
};
/** Core per-host tools. Plugins add theirs as host actions. */
function getSshActions(host: Host): {
type: TabType;
icon: React.ElementType;
label: string;
}[] {
const metricsEnabled = host.statsConfig?.metricsEnabled !== false;
return [
host.enableTerminal !== false &&
isTabTypeAvailable("terminal") && {
type: "terminal",
icon: Terminal,
label: "Terminal",
},
host.enableFileManager && {
type: "files",
icon: FolderSearch,
label: "Files",
},
host.enableDocker && { type: "docker", icon: Box, label: "Docker" },
// --- tmux-monitor --- opt-in per host, off by default
host.enableTerminal !== false &&
host.enableTmuxMonitor && {
@@ -108,16 +80,6 @@ function getSshActions(host: Host): {
label: "Tmux Monitor",
},
host.enableTunnel && { type: "tunnel", icon: Network, label: "Tunnels" },
metricsEnabled && {
type: "host-metrics",
icon: Activity,
label: "Host Metrics",
},
host.enableProxmoxStats === true && {
type: "proxmox-stats",
icon: HardDrive,
label: "Proxmox Stats",
},
].filter(Boolean) as {
type: TabType;
icon: React.ElementType;
@@ -135,7 +97,9 @@ export function CommandPalette({
onOpenPanel,
}: CommandPaletteProps) {
const { t } = useTranslation();
const { globallyEnabled: aiGloballyEnabled } = useAiAvailability();
const railItems = useRailItems();
const hostActions = useHostActions();
const paletteEntries = usePaletteEntries();
const inputRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState("");
const [recentActivity, setRecentActivity] = useState<RecentActivityItem[]>(
@@ -396,9 +360,8 @@ export function CommandPalette({
heading={t("commandPalette.navigation")}
className="px-2"
>
{[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
{[...railItems, ...RAIL_UTILITY_ITEMS]
.filter((item) => item.kind !== "tab")
.filter((item) => item.id !== "ai" || aiGloballyEnabled)
.map((item) => {
const Icon = item.icon;
return (
@@ -471,6 +434,37 @@ export function CommandPalette({
</>
)}
{paletteEntriesFor(paletteEntries, "global").length > 0 && (
<>
<CommandSeparator className="my-2" />
<CommandGroup
heading={t("commandPalette.pluginActions")}
className="px-2"
>
{paletteEntriesFor(paletteEntries, "global").map((entry) => {
const Icon = entry.icon;
return (
<CommandItem
key={`plugin-${entry.id}`}
value={`plugin-${entry.id} ${t(entry.titleKey)} ${(entry.keywords ?? []).join(" ")}`}
onSelect={() => handleAction(() => entry.run(shell))}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors">
{Icon && (
<Icon className="size-4 text-accent-brand" />
)}
</div>
<span className="text-sm font-semibold flex-1">
{t(entry.titleKey)}
</span>
</CommandItem>
);
})}
</CommandGroup>
</>
)}
{recentActivity.length > 0 && (
<>
<CommandSeparator className="my-2" />
@@ -483,17 +477,18 @@ export function CommandPalette({
key={item.id}
value={`recent-activity-${item.id}`}
onSelect={() =>
handleAction(() =>
onOpenTab(
ACTIVITY_TAB_TYPE[item.type],
item.hostName,
),
)
handleAction(() => {
const target = activityTarget(item.type);
if (target) onOpenTab(target.tab, item.hostName);
})
}
className="group flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-7 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors text-muted-foreground group-hover:text-accent-brand">
{ACTIVITY_ICONS[item.type]}
{(() => {
const Icon = activityTarget(item.type)?.icon;
return Icon ? <Icon className="size-3.5" /> : null;
})()}
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="text-sm font-semibold truncate">
@@ -537,17 +532,11 @@ export function CommandPalette({
value={`host-${host.id}`}
onSelect={() =>
handleAction(() => {
const type = host.enableSsh
? "terminal"
: host.enableRdp && isTabTypeAvailable("rdp")
? "rdp"
: host.enableVnc && isTabTypeAvailable("vnc")
? "vnc"
: host.enableTelnet &&
isTabTypeAvailable("telnet")
? "telnet"
: "terminal";
onOpenTab(type, host.name);
const action = defaultConnectAction(
hostActions,
host,
);
if (action) runHostAction(action, host, shell);
})
}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
@@ -599,59 +588,46 @@ export function CommandPalette({
</button>
),
)}
{host.enableSsh &&
((host.enableRdp && isTabTypeAvailable("rdp")) ||
(host.enableVnc && isTabTypeAvailable("vnc")) ||
(host.enableTelnet &&
isTabTypeAvailable("telnet"))) && (
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />
)}
{host.enableRdp && isTabTypeAvailable("rdp") && (
<button
title="RDP"
onClick={(e) => {
e.stopPropagation();
handleAction(() =>
onOpenTab("rdp", host.name),
);
}}
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
>
<Monitor className="size-3" />
RDP
</button>
)}
{host.enableVnc && isTabTypeAvailable("vnc") && (
<button
title="VNC"
onClick={(e) => {
e.stopPropagation();
handleAction(() =>
onOpenTab("vnc", host.name),
);
}}
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
>
<MousePointerClick className="size-3" />
VNC
</button>
)}
{host.enableTelnet &&
isTabTypeAvailable("telnet") && (
{hostActionsFor(hostActions, host).map((action) => {
const Icon = action.icon;
const label =
action.label?.(host) ?? t(action.titleKey);
return (
<button
title="Telnet"
key={action.id}
title={label}
onClick={(e) => {
e.stopPropagation();
handleAction(() =>
onOpenTab("telnet", host.name),
runHostAction(action, host, shell),
);
}}
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
>
<Terminal className="size-3" />
Telnet
<Icon className="size-3.5" />
</button>
)}
);
})}
{paletteEntriesFor(
paletteEntries,
"host",
host,
).map((entry) => {
const Icon = entry.icon;
return (
<button
key={entry.id}
title={t(entry.titleKey)}
onClick={(e) => {
e.stopPropagation();
handleAction(() => entry.run(shell, host));
}}
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
>
{Icon && <Icon className="size-3.5" />}
</button>
);
})}
{canEditHost(host) && (
<>
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />