Slash commands
Plugins can register top-level slash commands (buoy.addCommand) and full views (buoy.addView). Both occupy the same /<name> namespace; pick one per name.
Plugin commands and views are dispatched after every built-in (/ctx, /ns, /plugins, etc.), so a plugin can’t shadow them. Across plugins, the first to register a name wins (logged as a warning in the console).
buoy.addCommand
A fire-and-forget action triggered from the command bar.
buoy.addCommand({
name: "rollout-restart",
description: "Trigger `kubectl rollout restart` on a Deployment",
run: async (ctx) => {
// ctx.args — everything after `/rollout-restart `, trimmed
// ctx.context, ctx.namespace, ctx.impersonation — active app state
// ctx.notify(msg), ctx.runInBackground(label, fn), ctx.prompt(opts)
// ctx.openObject(...), ctx.openList(...), ctx.openView(...)
// ctx.mergePatch / strategicPatch / jsonPatch / statusPatch / delete
// — each takes an explicit { resource, name, namespace?, context? } target
const name = ctx.args.trim();
if (!name) {
ctx.notify("Usage: /rollout-restart <deployment>", { tone: "warn" });
return;
}
await ctx.runInBackground(`Restarting ${name}`, async () => {
await ctx.strategicPatch(
{
resource: "deployments",
namespace: ctx.namespace,
name,
},
{
spec: {
template: {
metadata: {
annotations: {
"kubectl.kubernetes.io/restartedAt": new Date().toISOString(),
},
},
},
},
},
);
});
ctx.notify(`Restarted ${name}`, { tone: "ok" });
},
});
Spec:
| field | required | notes |
|---|---|---|
name | ✓ | Lowercase letters / digits / dashes, 1–32 chars. |
description | Shown in any future autocomplete UI. | |
run | ✓ | Async callback. Throws surface as a parse-error banner. |
Tips:
- Parse your own args.
ctx.argsis the raw remainder. Split, regex, whatever. - For input you can’t pass via args, use
ctx.prompt(...)— it shows a modal and resolves with the values. - For multi-step work, wrap in
ctx.runInBackground(label, fn)so a spinner shows in the toaster. - Notify on success. Commands have no implicit feedback — surface a toast so the user knows it worked.
buoy.addView
A full top-level view (like /plugins or /contexts).
buoy.addView({
name: "scratch",
label: "Scratch",
description: "A scratchpad for ad-hoc CEL experiments",
render: ({ args }) => {
const [text, setText] = useAsync(async () => "(start typing)", []);
return (
<Section title="Scratchpad">
<pre>{args || text.data}</pre>
</Section>
);
},
});
Spec:
| field | required | notes |
|---|---|---|
name | ✓ | Slash name; same rules as addCommand. |
label | Tab title; defaults to name. | |
description | For future autocomplete. | |
render | ✓ | (ctx) => JSX. Re-renders as the live ctx ticks. |
The view’s ctx is the same AppContext as commands, plus args (the raw remainder of /<name> rest) and tick (1Hz counter for live-updating bits).
Async + feedback primitives
These are also available as free functions (notify, runInBackground, prompt) — calling notify(...) from anywhere in a plugin works the same as ctx.notify(...).
notify(message, opts?)
Bottom-right toast. Auto-dismisses after ttlMs (default 4000) or stays sticky when ttlMs: null. tone is "info" | "ok" | "warn" | "error".
runInBackground(label, fn, opts?)
Shows a spinner-and-label in the toaster while fn(task) runs. The task handle has setProgress(0..1) and setLabel(...). On success the entry flips to a green check; on failure, red — both auto-dismiss after ttlMs (4000 default) unless they failed.
await ctx.runInBackground("Syncing 12 manifests", async (task) => {
for (let i = 0; i < manifests.length; i++) {
task.setProgress((i + 1) / manifests.length);
task.setLabel(`Syncing ${manifests[i].name}`);
await sync(manifests[i]);
}
});
prompt({ title, fields, ... })
Shows a modal form; resolves with a {name: value} map, or null if the user hit Cancel / Escape.
const inputs = await ctx.prompt({
title: "Restart Deployment",
fields: [
{ kind: "text", name: "name", label: "Deployment name", required: true },
{ kind: "select", name: "scope", label: "Scope", options: [
{ value: "ns", label: "This namespace" },
{ value: "all", label: "All namespaces" },
]},
],
confirmLabel: "Restart",
});
if (!inputs) return;
// use inputs.name, inputs.scope
Field kinds: text, number, select, checkbox. Required fields show an error if left empty on submit.
useAsync(fn, deps)
React hook for one-shot async work inside a render fn:
buoy.addView({
name: "ratelimit",
render: () => {
const status = useAsync(async () => fetch("/api/quota").then((r) => r.json()), []);
if (status.loading) return <Banner tone="info" text="Loading…" />;
if (status.error) return <Banner tone="error" text={status.error} />;
return <KeyValue title="Quota"><Row label="Remaining" value={status.data.remaining} /></KeyValue>;
},
});
useAsync cancels stale runs when deps change. For live cluster state, prefer useObject / useList — they use real watches and update push-style.