mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 10:42:45 +00:00
* 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
318 lines
12 KiB
Markdown
318 lines
12 KiB
Markdown
---
|
|
title: Plugin quickstart
|
|
description: Build, install, share, and update a trusted Paseo plugin.
|
|
nav: Quickstart
|
|
order: 44
|
|
category: Plugins
|
|
---
|
|
|
|
# Plugin quickstart
|
|
|
|
A plugin is a TypeScript project installed into one Paseo daemon. It can add
|
|
[screens and sidebar items](/docs/plugins/reference#screens-and-sidebar-items),
|
|
[workspace panels](/docs/plugins/reference#workspace-panels),
|
|
[file panels](/docs/plugins/reference#file-panels),
|
|
[Command Center items](/docs/plugins/reference#command-center-items),
|
|
[slash commands](/docs/plugins/reference#slash-commands),
|
|
[composer pills](/docs/plugins/reference#composer-pills),
|
|
[timeline items](/docs/plugins/reference#timeline-items),
|
|
[themes](/docs/plugins/reference#contribute-a-theme),
|
|
[attachment sources](/docs/plugins/reference#add-a-composer-attachment-source), and
|
|
[daemon-side RPCs](/docs/plugins/reference#add-plugin-specific-backend-behavior). It can also
|
|
[connect a coding agent as a provider](/docs/plugins/providers). Client
|
|
contributions run on every Paseo client connected to that daemon, including mobile.
|
|
|
|
Browse [published plugins](https://paseo.sh/plugins), then install one on your daemon:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```text
|
|
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:
|
|
|
|
```tsx
|
|
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`:
|
|
|
|
```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](/docs/plugins/reference#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:
|
|
|
|
```bash
|
|
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](/docs/plugins/reference#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`:
|
|
|
|
```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](/docs/plugins/reference#theme-and-layout) for the token list.
|
|
|
|
Register the panel and a Command Center item that opens it by adding to `index.client.tsx`:
|
|
|
|
```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](https://lucide.dev/icons/) icon name, inline SVG, or a component. See
|
|
[Icons](/docs/plugins/reference#icons).
|
|
|
|
## Edit and reload
|
|
|
|
Source changes take effect only when you reload the plugin:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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](/docs/plugins/reference#plugin-sources) for identifier syntax. To share your own plugin, follow
|
|
[Publish a plugin](/docs/plugins/publishing).
|
|
|
|
Plugins published in a Git repository install by explicit Git shorthand or URL:
|
|
|
|
```bash
|
|
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](/docs/plugins/reference#plugin-sources) for exact syntax.
|
|
|
|
```bash
|
|
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](/docs/plugins/reference#cli-reference) for explicit targets and bulk updates.
|
|
|
|
## Read backend logs
|
|
|
|
Daemon-side handlers and cleanup can use normal Node logging:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```bash
|
|
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](/docs/plugins/reference#debug-backend-output) for retention and what not to
|
|
log.
|
|
|
|
## Next
|
|
|
|
- [Publish a plugin](/docs/plugins/publishing): share through npm, a private registry, or Git.
|
|
- [Build a provider plugin](/docs/plugins/providers): connect an agent directly or through ACP,
|
|
render provider-owned timeline items, test it, and publish it.
|
|
- [Plugin reference](/docs/plugins/reference): every contribution type, its fields, the runtime
|
|
modules, hosts, and the CLI.
|
|
- [Migrate a plugin to runtime entries](/docs/plugins/migration): move a plugin written against the
|
|
single `index.ts` entry, step by step.
|
|
- [TypeScript SDK](/docs/sdk): the workspace, agent, provider, and config API available as `paseo`
|
|
in client and server code.
|