fix(plugins): install local plugins on packaged desktop

The plugin compiler spawned esbuild through app.asar, and the subprocess
imported the React client barrel. Point esbuild at the unpacked binary,
add a react-free @paseo/plugin/server entry, and pass a typed PluginTheme
so plugin UI follows the active theme on desktop and mobile.
This commit is contained in:
Mohamed Boudra
2026-08-19 11:54:55 +02:00
parent ca2ee380df
commit 91024bc029
25 changed files with 643 additions and 153 deletions
+58 -17
View File
@@ -62,16 +62,34 @@ Cleanup can be async. Use it for timers, watchers, sockets, and other resources
## Add a workspace panel
Workspace panels live beside agents, terminals, files, and diffs. Their props contain stable IDs;
required-selector hooks read cached client state without subscribing to unrelated fields:
Workspace panels live beside agents, terminals, files, and diffs. Plugins run on desktop and
mobile, and Paseo has multiple themes. Every `Text` must take its color from `theme.colors`.
Use `layout.compact` for padding and stacking. Unstyled text is black and fails in dark themes.
```tsx
import { type PluginContext, type PluginWorkspacePanelProps, useWorkspace } from "@paseo/plugin";
import { Text } from "react-native";
import { useMemo } from "react";
import { Text, View } from "react-native";
function Overview({ workspaceId }: PluginWorkspacePanelProps) {
function Overview({ theme, layout, workspaceId }: PluginWorkspacePanelProps) {
const name = useWorkspace(workspaceId, (workspace) => workspace.name);
return <Text>{name}</Text>;
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 },
}),
[theme, layout.compact],
);
return (
<View style={styles.screen}>
<Text style={styles.title}>{name}</Text>
</View>
);
}
export default function contribute(plugin: PluginContext) {
@@ -102,24 +120,39 @@ the active workspace or agent. Command callbacks receive the selected host's `pa
## Add a sidebar surface
Plugin surfaces use React Native primitives and work across desktop, browser, iOS, and Android. Register the surface before its sidebar item:
Plugin surfaces use React Native primitives and work across desktop, browser, iOS, and Android. Register the surface before its sidebar item. Color text from `theme.colors` and pad from `layout.compact`.
```tsx
import type { PluginContext } from "@paseo/plugin";
import React, { useState } from "react";
import type { PluginContext, PluginSurfaceProps } from "@paseo/plugin";
import React, { useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
function Counter() {
function Counter({ theme, layout }: PluginSurfaceProps) {
const [count, setCount] = useState(0);
const styles = useMemo(
() => ({
screen: {
flex: 1,
padding: layout.compact ? 16 : 24,
gap: 16,
backgroundColor: theme.colors.surface0,
},
count: { color: theme.colors.foreground, fontSize: layout.compact ? 36 : 48 },
button: { padding: 14, borderRadius: 10, backgroundColor: theme.colors.accent },
buttonText: { color: theme.colors.accentForeground, textAlign: "center" as const },
}),
[theme, layout.compact],
);
return (
<View style={{ flex: 1, padding: 24, gap: 16 }}>
<Text style={{ fontSize: 48 }}>{count}</Text>
<View style={styles.screen}>
<Text style={styles.count}>{count}</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Increment counter, currently ${count}`}
onPress={() => setCount((value) => value + 1)}
style={styles.button}
>
<Text>Count me in</Text>
<Text style={styles.buttonText}>Count me in</Text>
</Pressable>
</View>
);
@@ -137,9 +170,14 @@ export default function contribute(plugin: PluginContext) {
}
```
Icons are Lucide icon names. `PluginSurfaceProps` provides `theme`, selected `host`, and `layout`. Validate the theme keys the surface reads. Paseo owns the route, header, host picker, close action, error boundary, and per-installation query client.
Icons are Lucide icon names. `theme` is a typed `PluginTheme` on every surface and panel. Primary text uses `theme.colors.foreground`; labels use `theme.colors.foregroundMuted`; the root view uses `theme.colors.surface0`. `layout.compact` is true on mobile and narrow windows. Paseo owns the route, header, host picker, close action, error boundary, and per-installation query client.
Client code may import `react`, `react-native`, `@tanstack/react-query`, `zod`, and `@paseo/plugin`. Install dependencies locally for typechecking; Paseo supplies these runtime modules.
Client code may import `react`, `react-native`, `@tanstack/react-query`, `zod`, `@paseo/plugin`, and `@paseo/plugin/server`. Install dependencies locally for typechecking; Paseo supplies these runtime modules.
| Module | Use it for |
| ---------------------- | ---------------------------------------------------- |
| `@paseo/plugin` | hooks and UI types |
| `@paseo/plugin/server` | `defineRpc`, `defineAttachmentSource`, handler types |
## Choose the correct API
@@ -183,7 +221,9 @@ The API covers workspaces, agents, providers, and daemon config. It omits connec
Define one Zod contract, register its subprocess handler, and call it with `useRpc()`:
```tsx
import { defineRpc, type PluginContext, useRpc } from "@paseo/plugin";
import type { PluginContext } from "@paseo/plugin";
import { useRpc } from "@paseo/plugin";
import { defineRpc } from "@paseo/plugin/server";
import { z } from "zod";
const greeting = defineRpc({
@@ -235,7 +275,8 @@ daemon log. Never log credentials or other secrets.
Define a search RPC and register a declarative source:
```tsx
import { defineAttachmentSource, defineRpc, type PluginContext } from "@paseo/plugin";
import type { PluginContext } from "@paseo/plugin";
import { defineAttachmentSource, defineRpc } from "@paseo/plugin/server";
import { z } from "zod";
const searchIssues = defineRpc({
@@ -328,7 +369,7 @@ After a change:
1. Run `npm run typecheck`.
2. Install or reload the exact runtime ID.
3. Run `paseo plugin ls` and require `running` with no error.
4. Confirm the contribution on the intended host. For UI work, check both a wide client and a compact/mobile client when available.
4. Confirm the contribution on the intended host. Open the Command Center with **⌘K** (macOS) or **Ctrl+K** (Windows/Linux). For UI work, check a wide desktop window and a compact/mobile client, and switch theme to confirm text still uses `foreground` / `foregroundMuted`.
5. Exercise the changed action or RPC, including its error state.
Common failures: