feat: add a permission-gated UI action registry and action slots for plugins

Moves the terminal toolbar's hardwired AI button into a slot the ssh-terminal
plugin offers and the ai plugin fills, gated on ai.services.use.
This commit is contained in:
LukeGus committed 2026-09-21 16:56:15 -05:00
1 parent 225f618337
commit 2d6c1cdecb
29 files changed
+1971 -40

No files matched your search

+44 -14
View File
@@ -13,26 +13,56 @@
* tabUtils.tsx (railView === "ai", case "ai"), the same way the terminal's
* tab is baked into Terminal.tsx rather than going through the generic tab
* registry. What gates it is isTabTypeAvailable("ai") in
* src/ui/shell/pluginLoader.ts's BUILT_IN_TABS_BY_PLUGIN map, which is
* consulted directly by AppShell.tsx, CommandPalette.tsx and HostItem.tsx --
* the same live mechanism ssh-terminal's "terminal" entry already uses.
* src/ui/shell/pluginLoader.ts's BUILT_IN_TABS_BY_PLUGIN map.
*
* register()/unregister() exist for parity with the other first-party
* plugins' frontend halves. Nothing invokes them yet: there is no frontend
* plugin loader (see src/ui/tests/sidebar/plugin-extension-seam.test.tsx),
* and the "ai" surface does not need registerRailItem/registerTabComponent
* in the first place, since it is gated through BUILT_IN_TABS_BY_PLUGIN
* instead.
* What this file DOES contribute is the terminal toolbar's AI button, through
* the action registry: ai.openWithContext, declared in manifest.json and
* gated on ai.services.use. The button used to be hardwired into
* TerminalToolbar.tsx behind a showAiAssistant prop.
*
* There is still no frontend plugin loader, so nothing imports this file yet
* (see src/ui/tests/sidebar/plugin-extension-seam.test.tsx). Until one exists,
* pluginLoader.ts performs the same registration itself when the ai plugin is
* enabled. This file is the version that runs once a loader lands, and the two
* must be kept in step.
*/
export const id = "ai";
export const tabId = "ai";
export const actionId = "ai.openWithContext";
export async function register() {
// Nothing to register: "ai" is a built-in rail-view/tab gated through
// isTabTypeAvailable, not a runtime-registered surface. See the header.
/** Asks the focused terminal to open its AI panel seeded with `context`. */
export function openWithContext(context) {
window.dispatchEvent(
new CustomEvent("termix:ai:openWithContext", {
detail: { context: typeof context === "string" ? context : "" },
}),
);
}
export async function unregister() {
// Nothing to unregister; see register().
export async function register({
registerAction,
registerSlotContribution,
icons,
}) {
registerAction(actionId, openWithContext, {
permission: "ai.services.use",
pluginId: id,
});
registerSlotContribution("terminal.toolbar", {
actionId,
titleKey: "ai.assistant",
icon: icons.Bot,
kind: "button",
pluginId: id,
});
}
export async function unregister({
unregisterAction,
unregisterSlotContribution,
}) {
unregisterSlotContribution("terminal.toolbar", actionId);
unregisterAction(actionId);
}
+12 -1
View File
@@ -36,7 +36,18 @@
"defaultForRole": {
"admin": ["ai.services.use"]
}
}
},
"actions": [
{
"id": "ai.openWithContext",
"titleKey": "ai.assistant",
"handler": "openWithContext",
"icon": "Bot",
"permission": "ai.services.use",
"slot": "terminal.toolbar",
"kind": "button"
}
]
},
"sidecars": []
}
+8
View File
@@ -12,10 +12,12 @@
export const id = "ssh-terminal";
export const tabId = "terminal";
export const toolbarSlotId = "terminal.toolbar";
export async function register({
registerRailItem,
registerTabComponent,
declareActionSlot,
icons,
}) {
registerTabComponent(tabId, () =>
@@ -30,12 +32,18 @@ export async function register({
labelKey: "nav.terminal",
kind: "tab",
});
// Offers other plugins a place in the terminal toolbar. The terminal does
// not know or care who fills it.
declareActionSlot({ id: toolbarSlotId, accepts: ["button"] });
}
export async function unregister({
unregisterRailItem,
unregisterTabComponent,
undeclareActionSlot,
}) {
unregisterRailItem(tabId);
unregisterTabComponent(tabId);
undeclareActionSlot(toolbarSlotId);
}
+6
View File
@@ -24,6 +24,12 @@
"icon": "SquareTerminal",
"openFrom": ["rail", "host-context-menu", "palette"]
}
],
"actionSlots": [
{
"id": "terminal.toolbar",
"accepts": ["button"]
}
]
},
"sidecars": []