Files
paseo/docs/plugins.md

4.2 KiB

Local plugins

Local plugins contribute daemon RPCs, native app surfaces, and composer attachment sources from one index.tsx. Paseo executes the server contribution in a subprocess and evaluates the client contribution in the app runtime. Plugin code is trusted code; this first slice does not sandbox it.

Configure a directory source

Add the plugin to the root plugins object in $PASEO_HOME/config.json:

{
  "plugins": {
    "my-plugin": {
      "source": "directory",
      "path": "/absolute/path/to/my-plugin"
    }
  }
}

The directory contains an identity-only manifest and one entry point:

my-plugin/
  paseo-plugin.json
  index.tsx
{
  "id": "my-plugin"
}

The manifest ID must match the config key. Restart the development daemon after changing plugin source or configuration; directory watching is outside the first slice.

Contribute behavior and UI

Default export one contribution function. Paseo calls it with a plugin-scoped context. The compiler removes UI registrations from the server bundle and RPC registrations from the client bundle before resolving dependencies.

import { Text } from "react-native";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import { defineRpc, type PluginContext, useRpc } from "@paseo/plugin";

const greetRpc = defineRpc({
  name: "greet",
  input: z.object({ name: z.string() }),
  output: z.object({ message: z.string() }),
});

function Greeting() {
  const greet = useRpc(greetRpc);
  const query = useQuery({
    queryKey: ["greeting", "Paseo"],
    queryFn: () => greet({ name: "Paseo" }),
  });
  return <Text>{query.data?.message}</Text>;
}

export default function contribute(plugin: PluginContext) {
  plugin.handle(greetRpc, async ({ name }) => ({ message: `Hello, ${name}` }));
  plugin.addSurface("main", Greeting);
  plugin.addSidebarItem({
    id: "main",
    title: "Greeting",
    icon: "MessageCircle",
    surface: "main",
  });
}

Paseo owns the route, screen header, Lucide icon validation, close action, theme DTO, layout facts, and render error boundary. The contributed component owns the complete body below the header.

RPC contracts validate inputs and outputs in both the app and plugin subprocess. useRpc returns a typed async function. Use the host-provided @tanstack/react-query for request state and caching; Paseo gives each plugin installation its own query client.

When the same plugin contribution exists on multiple hosts, Paseo shows it once in the sidebar and adds a host picker to the screen header. The selected host supplies the bundle, RPC transport, and query cache. Plugin code cannot address another host.

Contribute composer attachments

Register a declarative attachment source backed by a plugin RPC. Paseo owns the attachment menu, search picker, drafts, selected pill, and submission. The plugin returns complete text snapshots; credentials and vendor API calls stay in the daemon handler.

const searchIssues = defineRpc({
  name: "issues.search",
  input: z.object({ query: z.string() }),
  output: z.object({
    items: z.array(
      z.object({
        id: z.string(),
        identifier: z.string(),
        title: z.string(),
        subtitle: z.string().optional(),
        url: z.string().url(),
        text: z.string(),
        resourceType: z.string(),
      }),
    ),
  }),
});

const issues = defineAttachmentSource({
  id: "issues",
  title: "Acme issue",
  icon: "CircleDot",
  pickerTitle: "Attach Acme issue",
  searchPlaceholder: "Search by identifier or title",
  search: searchIssues,
});

export default function contribute(plugin: PluginContext) {
  plugin.handle(searchIssues, ({ query }) => searchAcmeIssues(query));
  plugin.addAttachmentSource(issues);
}

Attachment sources stay scoped to the composer's host. Unlike sidebar contributions, equal sources on several hosts are not coalesced. The selected snapshot submits as a text attachment with neutral external-resource presentation, so it remains readable if the plugin is removed or an older peer drops the optional presentation fields.

See examples/local-plugin for a native surface and extensions/linear for a complete official attachment-source extension.