mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
Merge remote-tracking branch 'origin/main' into diagnose/3589-contributor-6b87ee9f
This commit is contained in:
@@ -73,16 +73,16 @@ The required root manifest is `paseo-plugin.json`:
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Behavior |
|
||||
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Yes | Default installation ID. |
|
||||
| `name` | No | Non-empty display name for the registry and website; independent of the installation ID. |
|
||||
| `icon` | No | Relative path to a PNG inside the plugin package. |
|
||||
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
|
||||
| `description` | No | Non-empty summary shown below the plugin ID in **Settings → Plugins**. |
|
||||
| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). |
|
||||
| `requirements` | No | Supported Paseo versions, described below. |
|
||||
| `build` | No | Preparation commands, described in the CLI reference. |
|
||||
| Field | Required | Behavior |
|
||||
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Yes | Default installation ID. |
|
||||
| `name` | No | Non-empty display name for the registry, the website, your row in **Settings → Plugins**, and your settings header. Without it, Paseo shows the ID. |
|
||||
| `icon` | No | Relative path to a PNG inside the plugin package. |
|
||||
| `media` | No | Array of image or video paths inside the package, or HTTPS URLs. An empty array is valid. |
|
||||
| `description` | No | Non-empty summary shown below the plugin name in **Settings → Plugins**. |
|
||||
| `translations` | No | Listing text in other languages, read by the registry and ignored by Paseo. See [other languages](/docs/plugins/publishing#other-languages). |
|
||||
| `requirements` | No | Supported Paseo versions, described below. |
|
||||
| `build` | No | Preparation commands, described in the CLI reference. |
|
||||
|
||||
Paths are relative to `paseo-plugin.json`, use forward slashes, and cannot contain `..`
|
||||
segments. Registry listings for visual plugins require an interface image even though `media` is
|
||||
@@ -641,6 +641,13 @@ Cleanup can be async. Release timers, watchers, sockets, and other resources cre
|
||||
|
||||
Register async handlers with `server.addHook(name, handler)`. See [Hooks](/docs/plugins/hooks) for names, payloads, ordering, errors, and complete examples.
|
||||
|
||||
## Sidebar views
|
||||
|
||||
`addSidebarView({ id, title, description?, Component })` registers an optional replacement for the
|
||||
sidebar content region. It returns cleanup. The user selects one view in Settings > Sidebar;
|
||||
registration does not activate it. See [Build a sidebar view](sidebar-views.md) for the props,
|
||||
ref-based queries and actions, device state and theme tokens.
|
||||
|
||||
## Screens and sidebar items
|
||||
|
||||
A screen is a full page of plugin UI. A sidebar item is a component Paseo renders in the sidebar
|
||||
@@ -1309,8 +1316,11 @@ Themes need a host that supports them. A client released before `addTheme` canno
|
||||
## Settings screens
|
||||
|
||||
Register a component with `client.addSettingsScreen({ id, title, icon, Component })` in
|
||||
`index.client.tsx`. It appears under **Settings → Plugins → your plugin** on that host.
|
||||
`id` is unique within the installation; `icon` is any [icon](#icons) form. Registration returns an
|
||||
`index.client.tsx`. Your plugin's row in **Settings → Plugins** on that host then shows a settings
|
||||
button (cog) that opens your first screen. When you register several screens, the settings page shows
|
||||
one tab per screen, labeled with its `title` and `icon`, in registration order. With one screen there
|
||||
are no tabs. The page header shows your manifest `name`, or your plugin ID when you leave `name`
|
||||
out; it never includes a screen title. `id` is unique within the installation; `icon` is any [icon](#icons) form. Registration returns an
|
||||
idempotent remover, and plugin teardown removes remaining screens.
|
||||
|
||||
Call `client.openSettings(id)` or a Command Center callback's `openSettings(id)` to open one
|
||||
@@ -1391,13 +1401,13 @@ export default function contribute(server: PluginServerContext) {
|
||||
}
|
||||
```
|
||||
|
||||
| Definition field | Contract |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `id` | Lowercase identifier matching `[a-z][a-z0-9_-]*`; unique within the installation. |
|
||||
| `scope` | Required `"host"`. All authorized clients of that host share the document. No per-user, device-local, or cross-host synchronization. |
|
||||
| `version` | Required positive integer describing the schema, independent of the write revision. |
|
||||
| `schema` | Zod schema for JSON values. Supply defaults so parsing `{}` produces a complete document. |
|
||||
| `migrate(values, fromVersion)` | Optional synchronous or asynchronous conversion from an older stored version. Its output must pass the current schema. |
|
||||
| Definition field | Contract |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Lowercase identifier matching `[a-z][a-z0-9_-]*`; unique within the installation. |
|
||||
| `scope` | Required `"host"` or `"device"`. Host documents are shared by authorized clients. Device documents stay on this device, keyed by installation and settings ID; use them for layout preferences. |
|
||||
| `version` | Required positive integer describing the schema, independent of the write revision. |
|
||||
| `schema` | Zod schema for JSON values. Supply defaults so parsing `{}` produces a complete document. |
|
||||
| `migrate(values, fromVersion)` | Optional synchronous or asynchronous conversion from an older stored version. Its output must pass the current schema. |
|
||||
|
||||
Call `useSettings(preferences)` in any contributed component. It returns a discriminated state:
|
||||
|
||||
@@ -1904,7 +1914,10 @@ decisions.
|
||||
`client.addComposerPill({ id, workspaceId, agentId, button })` uses the same [button descriptor](#button-descriptor)
|
||||
and returns the same registration. It targets one agent's composer track alongside Tasks and
|
||||
Subagents. Composer pills always show the icon and `label` (or `title` when `label` is omitted).
|
||||
They never show a chevron, including for menus and popovers.
|
||||
Menus and popovers add an upward chevron on every layout, because their surface opens above the
|
||||
pill; actions show none, so a pill that runs on tap reads differently from one that opens something.
|
||||
The track keeps its space above the composer whether or not it has pills, so adding and removing
|
||||
pills does not move the transcript.
|
||||
|
||||
```tsx
|
||||
const pill = client.addComposerPill({
|
||||
@@ -2085,6 +2098,8 @@ function PullRequestAction({ theme }: PluginSurfaceProps) {
|
||||
|
||||
The returned API covers projects, workspaces, agents, terminals, providers, and daemon config. See the [SDK API reference](/docs/sdk/reference) for its methods. Connection lifecycle methods are intentionally absent because Paseo owns the connection.
|
||||
|
||||
To run a helper the user shouldn't see, such as a classifier or summarizer, create it as a [background agent](/docs/sdk/agents#run-a-helper-as-a-background-agent) in an existing workspace.
|
||||
|
||||
### Discover hosts and target another host
|
||||
|
||||
Use `useHosts()` to display configured hosts and `getPaseoClient(serverId)` in an action callback
|
||||
@@ -2229,10 +2244,168 @@ export default function contribute(server: PluginServerContext) {
|
||||
}
|
||||
```
|
||||
|
||||
Inputs and outputs are validated on both sides. RPC names start with a lowercase letter and contain lowercase letters, numbers, dots, hyphens, or underscores. `useRpc()` returns a typed async function. Use TanStack Query for request state, caching, and mutations.
|
||||
Inputs are parsed once in the plugin process; outputs are parsed once on the client. Send schema inputs from the client and return output-schema inputs from the handler; schema transforms run at the receiving side. RPC names start with a lowercase letter and contain lowercase letters, numbers, dots, hyphens, or underscores. `useRpc()` returns a typed async function. Use TanStack Query for request state, caching, and mutations.
|
||||
|
||||
Backend handlers receive the same `PaseoApi` as `{ paseo }`. Their connection belongs to the subprocess and closes when the plugin stops. It does not subscribe to timelines or catalog events until plugin code subscribes. Follow the [SDK event contract](/docs/sdk/events) for cleanup and timeline replacements. Backend code can use Node APIs and dependencies installed in the plugin directory.
|
||||
|
||||
## Stream events from the daemon {#subscriptions}
|
||||
|
||||
A subscription runs a daemon handler for as long as one client listens and sends that client typed
|
||||
events. Use it for watchers, tails, and live media; use plugin RPC for a single answer.
|
||||
|
||||
Define the contract in `shared/`:
|
||||
|
||||
```ts
|
||||
// shared/diff.ts
|
||||
import { defineSubscription } from "@getpaseo/plugin";
|
||||
import { z } from "zod";
|
||||
|
||||
export const diffWatch = defineSubscription({
|
||||
name: "diff.watch",
|
||||
input: z.object({ cwd: z.string(), from: z.string() }),
|
||||
event: z.object({ type: z.literal("changes"), files: z.array(z.string()) }),
|
||||
});
|
||||
```
|
||||
|
||||
Handle it in `index.server.ts`. The handler receives `push` and an abort `signal` next to `paseo`:
|
||||
|
||||
```ts
|
||||
server.handle(diffWatch, ({ cwd, from }, { push, signal }) => {
|
||||
const watcher = watchChanges(cwd, from, (files) => void push({ type: "changes", files }));
|
||||
signal.addEventListener("abort", () => watcher.close(), { once: true });
|
||||
});
|
||||
```
|
||||
|
||||
Subscribe from a surface with `useSubscription(contract, input, onEvent)`:
|
||||
|
||||
```tsx
|
||||
const [files, setFiles] = useState<string[]>([]);
|
||||
const subscription = useSubscription(diffWatch, { cwd, from: "HEAD" }, (event) => {
|
||||
setFiles(event.files);
|
||||
});
|
||||
if (subscription.status === "error") return <Text>{subscription.error.message}</Text>;
|
||||
```
|
||||
|
||||
Outside React, `client.subscribe` takes the same arguments plus options and returns a remover:
|
||||
|
||||
```ts
|
||||
const stop = client.subscribe(diffWatch, { cwd, from: "HEAD" }, onChanges, { signal });
|
||||
await stop.ready; // the daemon accepted the subscription
|
||||
stop(); // or await stop.release()
|
||||
```
|
||||
|
||||
RPCs and subscriptions share one name space; registering the same name for both fails the plugin
|
||||
load.
|
||||
|
||||
### Handler lifetime
|
||||
|
||||
| The handler... | Result |
|
||||
| --------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Throws synchronously, or the input is invalid | The subscribe call fails with the error. No events are sent. |
|
||||
| Returns nothing | The subscription stays open until the client releases it. |
|
||||
| Returns a promise that resolves | The subscription closes. |
|
||||
| Returns a promise that rejects | The subscription closes with the error. |
|
||||
|
||||
`signal` aborts when the client releases the subscription, unmounts the hook, changes its input,
|
||||
disconnects, or the plugin stops. Pushes after the abort are dropped. `push` validates the event
|
||||
against the contract and resolves once the event has left the plugin process; await it in a loop
|
||||
to pace a producer.
|
||||
|
||||
Each subscriber runs its own handler. Paseo does not share one handler between clients and does
|
||||
not replay missed events: after a reconnect or a plugin reload, mounted surfaces subscribe again and
|
||||
the handler starts from the beginning. Share an
|
||||
expensive watcher inside your plugin when several subscribers need it.
|
||||
|
||||
`useSubscription` returns a union on `status`:
|
||||
|
||||
| Status | Meaning |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `connecting` | Sent; the daemon has not accepted it yet. |
|
||||
| `live` | The handler is running. Events arrive through `onEvent`. |
|
||||
| `closed` | The handler finished. |
|
||||
| `error` | The subscribe failed, the handler failed, an event failed the contract, or the plugin stopped. |
|
||||
|
||||
The hook subscribes again when `input` changes by shallow equality. `onEvent` is always the latest
|
||||
render's callback, so an inline function does not resubscribe. `client.subscribe` takes
|
||||
`{ signal, onClosed }`; `onClosed` receives `{ error? }`.
|
||||
|
||||
### Bytes
|
||||
|
||||
A top-level `Uint8Array` crosses the wire as binary, without JSON or base64. Everything else is
|
||||
JSON. The rule applies to subscription events, RPC outputs, and RPC inputs. A Node `Buffer` is a
|
||||
`Uint8Array`.
|
||||
|
||||
```ts
|
||||
export const thumbnail = defineRpc({
|
||||
name: "thumbnail.render",
|
||||
input: z.object({ path: z.string(), width: z.number() }),
|
||||
output: z.instanceof(Uint8Array),
|
||||
});
|
||||
|
||||
// index.server.ts
|
||||
server.handle(thumbnail, ({ path, width }) => renderPng(path, width));
|
||||
|
||||
// client
|
||||
const renderThumbnail = useRpc(thumbnail);
|
||||
const png = await renderThumbnail({ path, width }); // Uint8Array
|
||||
```
|
||||
|
||||
Declare bytes with `z.instanceof(Uint8Array)`. An event can be bytes or JSON per push: declare a
|
||||
union and branch with `event instanceof Uint8Array`. A `Uint8Array` nested in an object fails
|
||||
validation on the sending side with an error that names the rule; send the bytes as their own
|
||||
event or RPC value and the metadata as a separate JSON event.
|
||||
|
||||
One event or RPC value carries at most 100 MiB less 65 KiB of framing. Push large payloads in
|
||||
chunks of a few hundred KiB; each `push` waits for the previous one to leave the plugin process.
|
||||
Uploads from a phone have a lower limit: Android closes the connection when one send exceeds
|
||||
16 MiB, so split a larger upload into several RPCs.
|
||||
|
||||
### Several streams on one subscription
|
||||
|
||||
Put a header byte in front of each payload to multiplex streams. Keep the layout in `shared/`, so
|
||||
the handler and the surface call named functions:
|
||||
|
||||
```ts
|
||||
// shared/media-frames.ts
|
||||
export const MediaOpcode = { Video: 0x01, Audio: 0x02 } as const;
|
||||
|
||||
export function encodeMediaFrame(opcode: number, payload: Uint8Array): Uint8Array {
|
||||
const bytes = new Uint8Array(1 + payload.byteLength);
|
||||
bytes[0] = opcode;
|
||||
bytes.set(payload, 1);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function decodeMediaFrame(bytes: Uint8Array) {
|
||||
return { opcode: bytes[0], payload: bytes.subarray(1) }; // a view, not a copy
|
||||
}
|
||||
|
||||
export const mediaStream = defineSubscription({
|
||||
name: "media.stream",
|
||||
input: z.object({ source: z.string() }),
|
||||
event: z.instanceof(Uint8Array),
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
useSubscription(mediaStream, { source }, (bytes) => {
|
||||
const frame = decodeMediaFrame(bytes);
|
||||
if (frame.opcode === MediaOpcode.Audio) audio.enqueue(frame.payload);
|
||||
else video.draw(frame.payload);
|
||||
});
|
||||
```
|
||||
|
||||
Events from one subscription arrive in push order, JSON and binary alike.
|
||||
`plugin-examples/media-stream` streams video and audio this way and uploads a frame back through
|
||||
an RPC with a `Uint8Array` input; `plugin-examples/diff-watch` watches a working tree.
|
||||
|
||||
### Older hosts
|
||||
|
||||
Subscriptions and binary RPC values need a host that advertises them. Against an older host,
|
||||
`useSubscription` reports `error` asking to update the host, `client.subscribe` rejects `ready`
|
||||
with the same message, and an RPC with a `Uint8Array` input or output fails. Raise your plugin's
|
||||
`requirements.paseo` to the release that ships subscriptions.
|
||||
|
||||
## Debug backend output
|
||||
|
||||
Backend contributions can write to stdout and stderr with normal Node logging:
|
||||
|
||||
Reference in New Issue
Block a user