* 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
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:
- Open Settings → Plugins on the target host.
- Paste
/absolute/path/to/workspace-plugininto Plugin source. - 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
- Publish a plugin: share through npm, a private registry, or Git.
- Build a provider plugin: connect an agent directly or through ACP, render provider-owned timeline items, test it, and publish it.
- Plugin reference: every contribution type, its fields, the runtime modules, hosts, and the CLI.
- Migrate a plugin to runtime entries: move a plugin written against the
single
index.tsentry, step by step. - TypeScript SDK: the workspace, agent, provider, and config API available as
paseoin client and server code.