Pivit

Commands

Function commands, interface commands, relevance scoring and typed arguments in the Pivit Plugin SDK.

A command is the unit of work in Pivit. Every plugin is a list of them.

Two kinds of command

Function commands implement execute. They do something and the bar closes.

defineCommand({
  id: "toggle-mute",
  name: "Toggle Mute",
  icon: "volume-off",
  execute: async () => {
    await toggleMute();
  }
});

Interface commands implement open. They return a React element that Pivit renders inside the bar.

defineCommand({
  id: "search-issues",
  name: "Search Issues",
  icon: "circle-dot",
  open: async (input) => <IssueList query={input} />
});

A command implements one or the other, never both. The type definitions enforce this, so execute and open on the same command is a compile error rather than a runtime surprise.

Relevance scoring

Without a resolve function, Pivit matches a command on its name. resolve lets you decide for yourself, and it is where a plugin stops feeling generic.

It receives the user's input and a scoring helper, and returns either a relevance score or null to stay hidden.

defineCommand({
  id: "open-repo",
  name: "Open Repository",
  icon: "github",
  resolve: (input, score) => {
    if (!input.startsWith("gh ")) {
      return null;
    }

    return {
      relevance: score.compose(
        score.prefix("gh", 2),
        score.fuzzy(input.slice(3))
      )
    };
  },
  execute: async (input) => {
    await openUrl(`https://github.com/${input.slice(3)}`);
  }
});

Scoring helpers

HelperUse it for
exact(value, weight?)Input matches exactly
prefix(value, weight?)Input begins with a trigger word
fuzzy(value, weight?)Loose, typo-tolerant matching
includes(value, weight?)Input appears anywhere
keywords(values, weight?)Any of several aliases matched
regex(pattern, value)Structured input, such as a URL or sum
length(min, value)Require a minimum input length
when(predicate, value)Conditional bonus
constant(value)A fixed score
compose(...scores)Combine several of the above

Returning null matters as much as returning a high score. A command that appears for every keystroke makes the whole bar noisier, and users blame the bar rather than the plugin.

Typed arguments

resolve can pass structured data to execute or open through args. The argument type flows through automatically, so parsing happens once.

defineCommand({
  id: "convert",
  name: "Convert Units",
  icon: "ruler",
  resolve: (input, score) => {
    const match = /^(\d+(?:\.\d+)?)\s*(km|mi)$/.exec(input.trim());

    if (!match) {
      return null;
    }

    return {
      args: { value: Number(match[1]), unit: match[2] as "km" | "mi" },
      relevance: score.regex(/^\d/, 3)
    };
  },
  execute: async (_input, args) => {
    // args is { value: number; unit: "km" | "mi" }
    await showToast({ title: formatConversion(args) });
  }
});

Display overrides and persistence

resolve can also change how the command presents itself for a particular input, and ask to stay pinned in the results.

return {
  display: { name: `Search GitHub for "${query}"`, icon: "search" },
  persistence: true,
  relevance: score.fuzzy(query)
};

persistence accepts true or a number, which keeps the command in the list for a set number of subsequent inputs. Use it for commands that act as a mode the user is working inside, not to force your way into results.

Permissions

Commands that reach outside Pivit declare what they need:

permissions: ["network", "clipboard"];

Pivit surfaces these to the user before a plugin is installed. Declare only what a command actually uses.

On this page