feat(plugins): split plugin code by runtime

Keep the entry point focused on contribution wiring so client UI and daemon handlers can use their own runtime APIs safely. Surface compilation failures and lifecycle transitions in the retained plugin log tail.
This commit is contained in:
Mohamed Boudra
2026-08-17 23:45:19 +02:00
parent 5ed1a852d1
commit 3cc8749437
17 changed files with 657 additions and 265 deletions
+4 -4
View File
@@ -25,9 +25,9 @@ Register the extension in `$PASEO_HOME/config.json`:
Restart the development daemon after changing its environment or plugin configuration.
The entry point demonstrates the complete attachment-source pattern:
The split entry point demonstrates the complete attachment-source pattern:
- define one validated search RPC;
- handle it in the daemon subprocess;
- register a declarative attachment source for the client;
- define the validated search RPC and attachment metadata in `issues.shared.ts`;
- handle it in the daemon subprocess from `issues.server.ts`;
- wire both contributions from `index.ts`;
- keep credentials and vendor API calls out of the client bundle.
+9
View File
@@ -0,0 +1,9 @@
import type { PluginContext } from "@paseo/plugin";
import { searchIssues } from "./issues.server";
import { issueAttachments, searchIssuesRpc } from "./issues.shared";
export default function contribute(plugin: PluginContext) {
plugin.handle(searchIssuesRpc, searchIssues);
plugin.addAttachmentSource(issueAttachments);
return () => {};
}
-46
View File
@@ -1,46 +0,0 @@
import { z } from "zod";
import {
defineAttachmentSource,
defineRpc,
type PluginAttachmentSearchPayload,
type PluginContext,
} from "@paseo/plugin";
import { createLinearIssueSearch } from "./linear";
const SearchPayloadSchema = z.object({
items: z.array(
z.object({
id: z.string(),
identifier: z.string(),
title: z.string(),
subtitle: z.string().optional(),
url: z.string().url(),
text: z.string(),
resourceType: z.string(),
}),
),
});
const searchIssues = defineRpc({
name: "issues.search",
input: z.object({ query: z.string() }),
output: SearchPayloadSchema,
});
const issueAttachments = defineAttachmentSource({
id: "issues",
title: "Linear issue",
icon: "CircleDot",
pickerTitle: "Attach Linear issue",
searchPlaceholder: "Search by identifier or title",
search: searchIssues,
});
export default function contribute(plugin: PluginContext) {
plugin.handle(searchIssues, async ({ query }): Promise<PluginAttachmentSearchPayload> => {
const linear = createLinearIssueSearch({ apiKey: process.env.LINEAR_API_KEY ?? "" });
return linear.search(query);
});
plugin.addAttachmentSource(issueAttachments);
return () => {};
}
+11
View File
@@ -0,0 +1,11 @@
import type { PluginAttachmentSearchPayload } from "@paseo/plugin";
import type { output as ZodOutput } from "zod";
import { searchIssuesRpc } from "./issues.shared";
import { createLinearIssueSearch } from "./linear";
export async function searchIssues({
query,
}: ZodOutput<typeof searchIssuesRpc.input>): Promise<PluginAttachmentSearchPayload> {
const linear = createLinearIssueSearch({ apiKey: process.env.LINEAR_API_KEY ?? "" });
return linear.search(query);
}
+31
View File
@@ -0,0 +1,31 @@
import { defineAttachmentSource, defineRpc } from "@paseo/plugin";
import { z } from "zod";
const SearchPayloadSchema = z.object({
items: z.array(
z.object({
id: z.string(),
identifier: z.string(),
title: z.string(),
subtitle: z.string().optional(),
url: z.string().url(),
text: z.string(),
resourceType: z.string(),
}),
),
});
export const searchIssuesRpc = defineRpc({
name: "issues.search",
input: z.object({ query: z.string() }),
output: SearchPayloadSchema,
});
export const issueAttachments = defineAttachmentSource({
id: "issues",
title: "Linear issue",
icon: "CircleDot",
pickerTitle: "Attach Linear issue",
searchPlaceholder: "Search by identifier or title",
search: searchIssuesRpc,
});
@@ -0,0 +1,9 @@
import type { output as ZodOutput } from "zod";
import { incrementRpc } from "./increment.shared";
export function increment(input: ZodOutput<typeof incrementRpc.input>) {
return {
value: input.value + 1,
handledBy: "plugin subprocess",
};
}
@@ -0,0 +1,8 @@
import { defineRpc } from "@paseo/plugin";
import { z } from "zod";
export const incrementRpc = defineRpc({
name: "increment",
input: z.object({ value: z.number() }),
output: z.object({ value: z.number(), handledBy: z.string() }),
});
+25
View File
@@ -0,0 +1,25 @@
import type { PluginContext } from "@paseo/plugin";
import { ExamplePanel } from "./main.client";
import { increment } from "./increment.server";
import { incrementRpc } from "./increment.shared";
export default function contribute(plugin: PluginContext) {
plugin.handle(incrementRpc, increment);
plugin.addWorkspacePanel({
id: "counter",
title: "Plugin counter",
icon: "Blocks",
context: "workspace",
Component: ExamplePanel,
});
plugin.addCommandCenterItem({
id: "open-counter",
title: "Open plugin counter",
icon: "Blocks",
context: "workspace",
onSelect({ openPanel }) {
openPanel("counter");
},
});
return () => {};
}
@@ -1,20 +1,9 @@
import { useMutation } from "@tanstack/react-query";
import { type PluginWorkspacePanelProps, useRpc, useWorkspace } from "@paseo/plugin";
import React, { useCallback, useMemo } from "react";
import { Pressable, Text, View } from "react-native";
import { useMutation } from "@tanstack/react-query";
import { z } from "zod";
import {
defineRpc,
type PluginContext,
type PluginWorkspacePanelProps,
useWorkspace,
useRpc,
} from "@paseo/plugin";
const incrementRpc = defineRpc({
name: "increment",
input: z.object({ value: z.number() }),
output: z.object({ value: z.number(), handledBy: z.string() }),
});
import { incrementRpc } from "./increment.shared";
const ExampleThemeSchema = z.object({
colors: z.object({
@@ -27,7 +16,7 @@ const ExampleThemeSchema = z.object({
}),
});
function ExamplePanel({ theme, workspaceId }: PluginWorkspacePanelProps) {
export function ExamplePanel({ theme, workspaceId }: PluginWorkspacePanelProps) {
const { colors } = ExampleThemeSchema.parse(theme);
const workspace = useWorkspace(workspaceId, ({ name }) => ({ name }));
const callIncrement = useRpc(incrementRpc);
@@ -67,27 +56,3 @@ function ExamplePanel({ theme, workspaceId }: PluginWorkspacePanelProps) {
</View>
);
}
export default function contribute(plugin: PluginContext) {
plugin.handle(incrementRpc, async (input) => ({
value: input.value + 1,
handledBy: "plugin subprocess",
}));
plugin.addWorkspacePanel({
id: "counter",
title: "Plugin counter",
icon: "Blocks",
context: "workspace",
Component: ExamplePanel,
});
plugin.addCommandCenterItem({
id: "open-counter",
title: "Open plugin counter",
icon: "Blocks",
context: "workspace",
onSelect({ openPanel }) {
openPanel("counter");
},
});
return () => {};
}