fix(server): plugin daemon sessions see all providers by pinning the daemon appVersion (#3902)

* fix(server): expose all providers to plugin sessions

* fix(app): route workspace actions semantically (#3896)

* fix(app): route workspace actions semantically

Separate Explorer visibility from view navigation, and give composer changes and pull request opens semantic routing owners. Unify diff destinations, keep compact PR navigation in Explorer, and derive maximize availability from visible workspace panes.

* test(app): cover composer changes routing

Update the browser journey for the Explorer-first composer action, then assert the second press follows the shared diff destination.

* test(server): track plugin runtime dependency parameter

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
Omer Cohen
2026-08-26 20:57:22 +00:00
committed by GitHub
co-authored by Mohamed Boudra paseo-bot[bot]
parent 0ad69d3eb0
commit 4de913fb82
9 changed files with 53 additions and 16 deletions
+1 -1
View File
@@ -609,7 +609,7 @@ export async function createPaseoDaemon(
});
const browserToolsPolicy = new DaemonConfigBrowserToolsPolicy(daemonConfigStore);
const browserToolsBroker = new BrowserToolsBroker({});
const pluginRuntime = new PluginService(logger, daemonConfigStore);
const pluginRuntime = new PluginService(logger, daemonConfigStore, daemonVersion);
const serverId = getOrCreateServerId(config.paseoHome, { logger });
const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger);
@@ -36,10 +36,10 @@ function createStore(
function createService(
home: string,
plugins: Record<string, { source: "directory"; path: string; enabled?: boolean }> = {},
runtime?: ConstructorParameters<typeof PluginService>[2],
runtime?: ConstructorParameters<typeof PluginService>[3],
): PluginService {
return bindTestSessionHost(
new PluginService(pino({ level: "silent" }), createStore(home, plugins), runtime),
new PluginService(pino({ level: "silent" }), createStore(home, plugins), "0.4.0", runtime),
);
}
@@ -225,7 +225,9 @@ describe("PluginService", () => {
`export default function contribute(plugin: unknown) { void plugin; return () => undefined; }`,
);
const store = createStore(home);
const service = bindTestSessionHost(new PluginService(pino({ level: "silent" }), store));
const service = bindTestSessionHost(
new PluginService(pino({ level: "silent" }), store, "0.4.0"),
);
await service.start();
await expect(
@@ -266,7 +268,9 @@ export default function contribute(plugin: unknown) {
}`,
);
const store = createStore(home);
const service = bindTestSessionHost(new PluginService(pino({ level: "silent" }), store));
const service = bindTestSessionHost(
new PluginService(pino({ level: "silent" }), store, "0.4.0"),
);
await service.start();
await service.installDirectory({ path: directory });
@@ -293,7 +297,9 @@ export default function contribute(plugin: unknown) {
`export default function contribute(plugin: unknown) { void plugin; return () => undefined; }`,
);
const store = createStore(home);
const service = bindTestSessionHost(new PluginService(pino({ level: "silent" }), store));
const service = bindTestSessionHost(
new PluginService(pino({ level: "silent" }), store, "0.4.0"),
);
await service.start();
await service.installDirectory({ path: first });
await service.installDirectory({ path: second });
@@ -321,7 +327,7 @@ export default function contribute(plugin: unknown) {
});
store.patch({ pluginsEnabled: false });
const paused = createPausedRuntime();
const service = new PluginService(pino({ level: "silent" }), store, paused.runtime);
const service = new PluginService(pino({ level: "silent" }), store, "0.4.0", paused.runtime);
await service.start();
store.patch({ pluginsEnabled: true });
@@ -343,7 +349,7 @@ export default function contribute(plugin: unknown) {
slow: { source: "directory", path: "/plugins/slow", enabled: false },
});
const paused = createPausedRuntime();
const service = new PluginService(pino({ level: "silent" }), store, paused.runtime);
const service = new PluginService(pino({ level: "silent" }), store, "0.4.0", paused.runtime);
await service.start();
const enabling = expect(service.enablePlugin("slow")).rejects.toThrow(
@@ -366,7 +372,7 @@ export default function contribute(plugin: unknown) {
slow: { source: "directory", path: "/plugins/slow", enabled: false },
});
const paused = createPluginSelectivePausedRuntime("occupier");
const service = new PluginService(pino({ level: "silent" }), store, paused.runtime);
const service = new PluginService(pino({ level: "silent" }), store, "0.4.0", paused.runtime);
await service.start();
const occupying = service.enablePlugin("occupier");
+2 -1
View File
@@ -34,7 +34,8 @@ export class PluginService {
constructor(
logger: pino.Logger,
private readonly configStore: DaemonConfigStore,
runtime: PluginRuntimePort = new PluginRuntime(logger),
daemonVersion: string,
runtime: PluginRuntimePort = new PluginRuntime(logger, daemonVersion),
) {
this.logger = logger.child({ module: "plugin-service" });
this.runtime = runtime;
@@ -4,6 +4,7 @@ import path from "node:path";
import { afterEach, expect, test } from "vitest";
import { DaemonClient } from "../test-utils/daemon-client.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { createTestAgentClient, createTestAgentClients } from "../test-utils/fake-agent-client.js";
const roots: string[] = [];
@@ -30,6 +31,12 @@ const create = defineRpc({
output: z.object({ workspaceId: z.string(), agentId: z.string() }),
});
const list = defineRpc({
name: "list",
input: z.object({}),
output: z.object({ agentIds: z.array(z.string()) }),
});
export default function contribute(plugin: PluginContext) {
plugin.handle(create, async ({ path }, { paseo }) => {
const workspace = await paseo.workspaces.create({
@@ -37,16 +44,22 @@ export default function contribute(plugin: PluginContext) {
title: "Plugin workspace",
});
const agent = await workspace.agents.create({
config: { provider: "codex/test" },
config: { provider: "pi/test" },
prompt: "Created by a plugin handler",
});
return { workspaceId: workspace.id, agentId: agent.id };
});
plugin.handle(list, async (_input, { paseo }) => {
const result = await paseo.agents.list({ page: { limit: 100 } });
return { agentIds: result.entries.map((entry) => entry.agent.id) };
});
return () => undefined;
}`,
);
const daemon = await createTestPaseoDaemon();
const daemon = await createTestPaseoDaemon({
agentClients: { ...createTestAgentClients(), pi: createTestAgentClient("pi") },
});
const client = new DaemonClient({
url: `ws://127.0.0.1:${daemon.port}/ws`,
appVersion: "0.4.0",
@@ -71,6 +84,10 @@ export default function contribute(plugin: PluginContext) {
if (typeof created !== "object" || created === null) {
throw new Error("Plugin returned an invalid creation result");
}
const listed = await client.invokePluginRpc("paseo-api", "list", {});
expect(listed).toEqual({
agentIds: expect.arrayContaining([Reflect.get(created, "agentId")]),
});
await client.removePlugin("paseo-api");
const workspaces = await client.fetchWorkspaces();
const agents = await client.fetchAgents();
@@ -1,5 +1,5 @@
export type PluginProcessRequest =
| { type: "initialize"; pluginId: string; bundle: string }
| { type: "initialize"; pluginId: string; bundle: string; appVersion: string }
| { type: "invoke"; requestId: string; method: string; input: unknown }
| { type: "shutdown" }
| { type: "paseo_frame"; data: string | Uint8Array; isBinary: boolean }
@@ -99,6 +99,7 @@ async function initialize(message: Extract<PluginProcessRequest, { type: "initia
url: `ipc://plugin/${encodeURIComponent(message.pluginId)}`,
clientId: `plugin:${message.pluginId}`,
clientType: "cli",
appVersion: message.appVersion,
reconnect: { enabled: false },
transportFactory,
});
@@ -63,10 +63,10 @@ function createReloadChild(name: string, events: string[], methods: string[] = [
}
function createTestRuntime(
dependencies: NonNullable<ConstructorParameters<typeof PluginRuntime>[1]> = {},
dependencies: NonNullable<ConstructorParameters<typeof PluginRuntime>[2]> = {},
logger = pino({ level: "silent" }),
): PluginRuntime {
return new PluginRuntime(logger, {
return new PluginRuntime(logger, "0.4.0", {
...dependencies,
sessionHost: dependencies.sessionHost ?? {
async attachPluginSocket(_pluginId, socket) {
@@ -204,7 +204,11 @@ export class PluginRuntime {
private sessionHost: PluginPaseoSessionHost | null;
private readonly listeners = new Set<(pluginId: string, error?: string) => void>();
constructor(logger: pino.Logger, dependencies: PluginRuntimeDependencies = {}) {
constructor(
logger: pino.Logger,
private readonly daemonVersion: string,
dependencies: PluginRuntimeDependencies = {},
) {
this.logger = logger.child({ module: "plugins" });
this.spawnChild = dependencies.spawnChild ?? spawnPluginChild;
this.sessionHost = dependencies.sessionHost ?? null;
@@ -363,6 +367,7 @@ export class PluginRuntime {
void send(child, {
type: "initialize",
pluginId,
appVersion: this.daemonVersion,
bundle: bundles.serverBundle,
}).catch(fail);
});
@@ -1281,3 +1281,10 @@ export function createTestAgentClients(
opencode: new FakeAgentClient("opencode", options),
};
}
export function createTestAgentClient(
provider: string,
options: TestAgentClientOptions = {},
): AgentClient {
return new FakeAgentClient(provider, options);
}