feat: add host sections, tab embeds and the operations client to the plugin ui

This commit is contained in:
LukeGus committed 2026-11-03 22:34:29 -06:00
1 parent cc725c7889
commit 631f6ac9f7
12 files changed
+184 -3

No files matched your search

+28
View File
@@ -79,6 +79,34 @@ export function createPluginApi(pluginId: string): AxiosInstance {
}) as AxiosInstance;
}
/**
* An axios client rooted at core's /operations routes: the action catalog,
* runs, approvals, events, signals and cross host resources.
*/
export function createOperationsApi(): AxiosInstance {
return new Proxy(authApi, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
if (typeof value !== "function") return value;
if (
property === "get" ||
property === "delete" ||
property === "post" ||
property === "put" ||
property === "patch"
) {
return (url: string, ...rest: unknown[]) =>
(value as (...args: unknown[]) => unknown).call(
target,
`/operations${url.startsWith("/") ? url : `/${url}`}`,
...rest,
);
}
return value.bind(target);
},
}) as AxiosInstance;
}
/**
* A raw fetch on this plugin's /plugin-api mount point, for a response the
* caller reads as a stream (server-sent events). axios buffers the whole
+19
View File
@@ -41,6 +41,10 @@ import {
type KeybindingEditorProps,
} from "@/shell/keybinding-registry";
import { registerHostProtocol } from "@/sidebar/host-protocols";
import {
registerHostSection,
type HostSectionRenderProps,
} from "@/shell/host-sections";
import {
registerDashboardCard,
type DashboardCardRenderProps,
@@ -424,6 +428,21 @@ export function createPluginApp(
);
},
registerHostSection(section) {
return track(
registerHostSection({
id: section.id,
pluginId,
titleKey: key(section.titleKey),
order: section.order,
when: hostCallback(section.when ?? (() => true)) as never,
component: scoped(
section.component as unknown as ComponentType<HostSectionRenderProps>,
),
}),
);
},
registerHostContextMenuItem(item) {
return track(
registerHostContextMenuItem({
+10
View File
@@ -823,3 +823,13 @@ export {
rightDockableIds,
useRailItems,
} from "@/sidebar/rail-items";
export {
hostSectionsFor,
type HostSectionDef,
listHostSections,
useHostSections,
} from "@/shell/host-sections";
export { HostTabEmbed } from "@/shell/HostTabEmbed";
import { createOperationsApi } from "@/lib/plugin-transport";
/** Core's /operations routes, with the user's own session. */
export const operationsApi = createOperationsApi();
+1
View File
@@ -471,6 +471,7 @@ export {
resetHostContributions,
} from "@/sidebar/host-contributions";
export { registerHostProtocol } from "@/sidebar/host-protocols";
export { registerHostSection, resetHostSections } from "@/shell/host-sections";
export {
RAIL_ITEMS,
registerRailItem,
+45
View File
@@ -0,0 +1,45 @@
import { useMemo } from "react";
import type { Host, Tab } from "@/types/ui-types";
import type { TabShellCallbacks } from "@/shell/tab-registry";
import { renderTabContent } from "@/shell/tabUtils";
/**
* Draws a registered tab type inside another view, such as a host page, with
* the same props the tab stage gives it. The embed has its own instance id, so
* it never collides with the same tool opened as a normal tab.
*/
export function HostTabEmbed({
host,
type,
label,
data,
isVisible = true,
shell,
}: {
host: Host;
type: string;
label?: string;
data?: Record<string, unknown>;
isVisible?: boolean;
shell: TabShellCallbacks;
}) {
const tab = useMemo<Tab>(
() => ({
id: `embed-${type}-${host.id}`,
instanceId: `embed-${type}-${host.id}`,
type,
label: label ?? host.name ?? host.ip,
host,
openedAt: Date.now(),
data,
}),
// A new host or tool is a new tab; label and data changes keep it.
// eslint-disable-next-line react-hooks/exhaustive-deps
[type, host.id],
);
return (
<div className="h-full min-h-0 flex flex-col">
{renderTabContent(tab, { shell, isVisible, isFocusedPane: isVisible })}
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
import type { ComponentType } from "react";
import type { Host } from "@/types/ui-types";
import { byOrderThenId, createRegistry } from "@/lib/registry";
import type { TabShellCallbacks } from "@/shell/tab-registry";
export interface HostSectionRenderProps {
host: Host;
isVisible: boolean;
shell: TabShellCallbacks;
}
/**
* A card a plugin adds to a host page. The page itself belongs to a plugin;
* core only keeps the list so any plugin can fill it.
*/
export interface HostSectionDef {
id: string;
pluginId?: string;
titleKey: string;
order?: number;
when: (host: Host) => boolean;
component: ComponentType<HostSectionRenderProps>;
}
const sections = createRegistry<HostSectionDef>(byOrderThenId);
export const registerHostSection = sections.register;
export const useHostSections = sections.useList;
export const listHostSections = sections.list;
export const resetHostSections = sections.reset;
export function hostSectionsFor(
all: HostSectionDef[],
host: Host,
): HostSectionDef[] {
return all.filter((section) => {
try {
return section.when(host);
} catch {
return false;
}
});
}