Files
paseo/public-docs/plugins/index.md
T
Mohamed Boudra e464fd3bd3 feat(plugins): file panels (#7411)
* fix(files): report document MIME types for binary files

File reads now carry the document's MIME type for binary files, so a file panel can
hand the bytes to a viewer that needs the real type (application/pdf).

* feat(plugins): file panels

Plugins contribute file panels with addFilePanel. A file tab hosts the panels that claim the
file: the built-in Markdown, HTML and image panels, plugin panels, and Source, Paseo's editor,
which claims every text file and is always the last candidate. The panel is the tab body;
the bar shows the document's status and a switcher between the file's panels (side by side
up to three, two on a phone, a menu beyond that).

Paseo asks which panel opens a file only when a plugin panel is among several candidates,
once per open, and remembers the answer per extension or exact filename when told to. Open
with in the tab menu and the switcher change the tab's panel; Always use remembers it.
Opening a file at a line switches the tab to Source.

Panels read and write the host's document, so dirty state, autosave and conflicts stay
Paseo's. DocumentFrame shows bytes documents such as PDFs in the browser's viewer.

Replaces the file tab's Preview/Source mode toggle.

* feat(file-panels): choose and forget file panels in Settings > Files

Settings > Files lists each extension or filename where a plugin panel competes with
another candidate, or that has a remembered panel. Each row chooses the panel that opens
it, or Ask every time; Ask again forgets the choice. Source appears as a candidate for
text files. Panels no connected host offers can be forgotten but not chosen.

* docs(plugins): file panel examples and author reference

Adds the pdf-panel and markdown-panel examples, the File panels section of the plugin
reference, and an e2e spec that installs both examples, opens a PDF in the browser
viewer, and edits Markdown through Paseo's document across the switcher, Source,
autosave and conflicts.

* feat(file-panels): panels read and write through a file handle

The file tab stats the file and mounts its panel without reading it. Every
panel, built-in or plugin, gets a PluginFile handle: readText, readBytes, url,
version-checked writeText/writeBytes (PluginFileConflictError unless force),
and watch. useFileText, useFileBytes and useFileUrl read through it, and
Loading joins @getpaseo/plugin/client/ui. Panels own their loading state.

The host document pipeline, the explorer preview cache and the preview
lifecycle are deleted. The daemon classifies a file version from a bounded
sniff and writes base64 content as bytes behind fileByteWrites.

A deleted file keeps its panel mounted; a read confirms the deletion first,
so a remove-and-rewrite is not reported as one. Native Source decodes with
builtins and highlights in idle-time slices, the lines around the opening
line first, so a large file no longer blocks the JS thread.

* fix(file-panels): Settings offers Source where the picker does, and review follow-ups

- Settings > Files offers Source on every key except those a built-in panel of
  another kind (Image) claims, so a plugin that alone claims a text extension
  shows the same choices as the picker.
- Delete the unused isSourceFilePanel.
- The Markdown example warns with the file name when its unmount write fails,
  rather than dropping the edit silently.
- Document that PluginFile stat fields are read at render time and that watch
  or the read hooks are how a panel learns they changed.

* fix(plugins): file events are objects, deletion is a typed error, DocumentFrame owns its fallback

- watch listeners receive { type: "changed" } | { type: "deleted" }, so later
  versions can add fields without breaking them.
- PluginFileDeletedError joins PluginFileConflictError. The read hooks report a
  deleted file with it, and a write over a deleted file rejects with it; Source
  shows that as the deleted notice. PluginFileConflictError now means only
  "changed on disk".
- DocumentFrame on Android shows Paseo's "<TYPE> files can't be shown on this
  device" message for documents its WebView cannot display, so the PDF example
  is useFileUrl + Loading + DocumentFrame with no platform branch.

* test(file-panels): Settings offers Source for a remembered panel no host has

* test(e2e): hide the developer error toast instead of clicking it away

The locator handler that dismissed #error-toast could match a toast that closed before its
click, then wait on it until the test timed out (agent-timeline-resume.spec.ts:45 failed every
attempt on this branch). Hiding the toast with CSS keeps it off app actions with no race.

* test(e2e): desktop browser-tabs hides developer toasts on its attached page
2026-10-21 10:49:04 +07:00

12 KiB

title, description, nav, order, category
title description nav order category
Plugin quickstart Build, install, share, and update a trusted Paseo plugin. Quickstart 44 Plugins

Plugin quickstart

A plugin is a TypeScript project installed into one Paseo daemon. It can add screens and sidebar items, workspace panels, file panels, Command Center items, slash commands, composer pills, timeline items, themes, attachment sources, and daemon-side RPCs. It can also connect a coding agent as a provider. Client contributions run on every Paseo client connected to that daemon, including mobile.

Browse published plugins, then install one on your daemon:

paseo plugin add owner/slug

Or paste owner/slug into Settings → Plugins → Plugin source and select Install plugin. Paseo installs the registry's reviewed artifact. Use git:owner/repository or a full Git URL when you want to install directly from a Git repository.

This guide scaffolds a plugin, runs it, and adds a workspace panel to it.

Create a plugin

Use an absolute path on the daemon machine:

paseo plugin init /absolute/path/to/workspace-plugin
cd /absolute/path/to/workspace-plugin
npm install

init writes a strict TypeScript project and does not run the package manager. npm install adds development dependencies for typechecking and tests only; Paseo supplies the plugin SDK, React, React Native, TanStack Query, and Zod at runtime.

The scaffold is a working plugin: a screen, opened from the sidebar, with a button that asks the daemon for a greeting through an RPC.

workspace-plugin/
  paseo-plugin.json      # plugin ID and supported Paseo versions
  index.client.tsx       # runs in the Paseo app
  index.server.ts        # runs in a daemon subprocess
  client/greeting.tsx    # the screen component
  client/web.ts          # the only file allowed to touch browser APIs
  server/greeting.ts     # the RPC handler
  shared/greeting.ts     # the RPC contract, imported by both
  package.json
  tsconfig.json

Each entry default-exports one function that registers contributions and returns a cleanup function. index.client.tsx registers the screen and the sidebar item that opens it:

import type { PluginClientContext, PluginSidebarItemProps } from "@getpaseo/plugin/client";
import { SidebarRow } from "@getpaseo/plugin/client/ui";
import { GreetingScreen } from "./client/greeting";

function GreetingItem({ currentScreen, openScreen }: PluginSidebarItemProps) {
  return (
    <SidebarRow
      icon="MessageCircle"
      active={currentScreen?.screenId === "greeting"}
      onPress={() => openScreen({ screenId: "greeting" })}
    />
  );
}

export default function contribute(client: PluginClientContext) {
  client.addScreen({ id: "greeting", title: "Greeting", Component: GreetingScreen });
  client.addSidebarHeaderItem({ id: "greeting", title: "Greeting", Component: GreetingItem });
  return () => {};
}

index.server.ts registers the handler for the contract in shared/greeting.ts:

import type { PluginServerContext } from "@getpaseo/plugin/server";
import { createGreeting } from "./server/greeting";
import { greetingRpc } from "./shared/greeting";

export default function contribute(server: PluginServerContext) {
  server.handle(greetingRpc, createGreeting);
  return () => {};
}

The directory is the boundary. Code under client/ compiles only into the app bundle, code under server/ only into the daemon bundle, and shared/ into both. Importing across that line, adding a code file at the root, or importing a node: module from client code is a compile error. A plugin with no daemon-side work can omit index.server.ts; a plugin with no UI can omit index.client.tsx.

Client code runs on phones as well as in browsers. The project typechecks without the DOM library, so document and window are errors outside client/web.ts, which shows how to gate a browser API behind Platform.OS with a native fallback. See Cross-platform rules before writing UI.

Install and try it

Plugins are trusted, unsandboxed code: server code and preparation commands run with the daemon user's access on the daemon machine, and client code runs inside the Paseo app. Installing a plugin means you trust that codebase, its dependencies, and its future updates.

Turn on Enable plugins under Settings → Plugins on the daemon you are installing into. It is the global switch for every plugin on that daemon. It is also the root pluginsEnabled field in the daemon's config.json; after editing the file, apply it with paseo reload --json. An automated tool must read the current value and get your explicit permission before turning it on.

Then typecheck and install:

npm run typecheck
paseo plugin install /absolute/path/to/workspace-plugin
paseo plugin ls

Or install the directory from the app:

  1. Open Settings → Plugins on the target host.
  2. Paste /absolute/path/to/workspace-plugin into Plugin source.
  3. Select Install plugin. The app uses the plugin ID from paseo-plugin.json.

The source field also accepts Git repositories, npm packages, and plugin subdirectories. See Plugin sources for the accepted syntax and resolution rules. If the form asks you to update the host, update that host before installing; the existing management actions remain available.

Each installed plugin row shows its status and description. Use its switch to enable or disable it, and open its three-dot menu for settings, logs, reload, and removal.

paseo plugin ls should report the plugin as running. Open Paseo, choose Greeting in the sidebar, and press Create greeting. The message comes back from the daemon subprocess through the RPC.

If the sidebar item is missing, check that Enable plugins is on, the plugin is running, and the client is viewing the host you installed into. paseo plugin logs workspace-plugin shows the daemon-side output, including load errors.

Add a workspace panel

A workspace panel opens as a tab next to agents, terminals, and files. Create client/overview.tsx:

import { type PluginWorkspacePanelProps, useWorkspace } from "@getpaseo/plugin/client";
import { useMemo } from "react";
import { Text, View } from "react-native";

export function WorkspaceOverview({ theme, layout, workspaceId }: PluginWorkspacePanelProps) {
  const workspace = useWorkspace(workspaceId, ({ name, directory }) => ({
    name,
    directory,
  }));
  const styles = useMemo(
    () => ({
      screen: {
        flex: 1,
        padding: layout.compact ? 16 : 24,
        gap: layout.compact ? 8 : 12,
        backgroundColor: theme.colors.surface0,
      },
      title: { color: theme.colors.foreground, fontSize: layout.compact ? 20 : 24 },
      label: { color: theme.colors.foregroundMuted },
      detail: { color: theme.colors.foreground },
    }),
    [theme, layout.compact],
  );

  return (
    <View style={styles.screen}>
      <Text style={styles.title}>{workspace?.name}</Text>
      <Text style={styles.label}>Directory</Text>
      <Text style={styles.detail}>{workspace?.directory}</Text>
    </View>
  );
}

useWorkspace reads the fields the panel renders from the app's cached state, without an RPC and without re-rendering when unrelated fields change. Every Text takes its color from theme.colors, and layout.compact drives spacing, so the panel works in every Paseo theme and on phones. See Theme and layout for the token list.

Register the panel and a Command Center item that opens it by adding to index.client.tsx:

import { WorkspaceOverview } from "./client/overview";

// Inside contribute(client), after the existing registrations:
client.addWorkspacePanel({
  id: "overview",
  title: "Workspace overview",
  icon: "PanelsTopLeft",
  context: "workspace",
  locations: ["workspace", "explorer"],
  Component: WorkspaceOverview,
});
client.addCommandCenterItem({
  id: "open-overview",
  title: "Open workspace overview",
  icon: "PanelsTopLeft",
  context: "workspace",
  onSelect({ openPanel }) {
    openPanel("overview");
  },
});

icon is a Lucide icon name, inline SVG, or a component. See Icons.

Edit and reload

Source changes take effect only when you reload the plugin:

npm run typecheck
paseo plugin reload workspace-plugin

A reload stops the old plugin, runs its cleanup, compiles the current source, and starts it again. A failed reload stays failed and reports its error in paseo plugin ls; fix the source and reload again.

Open a workspace, press ⌘K on macOS or Ctrl+K on Windows and Linux, and choose Open workspace overview. The panel opens as a workspace tab.

Install a published plugin

Install a trusted npm plugin using npm on the daemon host:

paseo plugin install npm:@acme/paseo-review@1.2.0

Or paste the same identifier into Settings → Plugins → Plugin source and select Install plugin. The installed row uses the manifest ID and provides the same enable switch, Logs, Reload, and Remove actions as a directory plugin. npm is needed for acquisition; subsequent loading and reload use the installed files.

See plugin sources for identifier syntax. To share your own plugin, follow Publish a plugin.

Plugins published in a Git repository install by explicit Git shorthand or URL:

paseo plugin add git:owner/repository
paseo plugin add https://gitlab.com/group/repository.git
paseo plugin add git:owner/monorepo:plugins/workspace
paseo plugin add git:owner/repository --ref main

Append :relative/path when the plugin lives below the repository root. --ref selects the initial Git content once; omitting it installs the default HEAD. See the source reference for exact syntax.

paseo plugin ls
paseo plugin update workspace-plugin
paseo plugin update --all

ls reports source identity and the current installed revision without contacting the remote. update shows a proposed npm or Git revision and asks for approval. npm offers a newer latest release; Git offers the current default HEAD, regardless of the initial install selector. Use --check to preview only or --yes to approve without a prompt. See the CLI reference for explicit targets and bulk updates.

Read backend logs

Daemon-side handlers and cleanup can use normal Node logging:

console.log("Refreshing issues");
console.error("Issue refresh failed", error);

Read the recent output by opening the plugin's three-dot menu under Settings → Plugins and selecting Logs, or use the CLI:

paseo plugin logs workspace-plugin
paseo plugin logs workspace-plugin --json

The tail includes [paseo] loading, ready, stopping, and stopped entries, plus compilation and load failures, and it survives reloads and crashes. Client-side output stays in the app. See Debug backend output for retention and what not to log.

Next