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:

fieldrequirednotes
nameLowercase letters / digits / dashes, 1–32 chars.
descriptionShown in any future autocomplete UI.
runAsync callback. Throws surface as a parse-error banner.

Tips:

  • Parse your own args. ctx.args is 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:

fieldrequirednotes
nameSlash name; same rules as addCommand.
labelTab title; defaults to name.
descriptionFor 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.


Edit this page on GitLab