feat(client): expose project list in public SDK (#3899)

This commit is contained in:
Omer Cohen
2026-08-27 07:54:20 +00:00
committed by GitHub
parent 4c46f20179
commit 657152c47a
5 changed files with 99 additions and 4 deletions
+2 -2
View File
@@ -164,8 +164,8 @@ RPC contracts validate inputs and outputs in both the app and plugin subprocess.
typed async function. Use the host-provided `@tanstack/react-query` for request state and caching;
Paseo gives each plugin installation its own query client.
`usePaseo()` and the handler's `{ paseo }` context expose the same `PaseoApi`: workspaces, agents,
providers, and daemon config. They do not expose connection lifecycle. A surface borrows the
`usePaseo()` and the handler's `{ paseo }` context expose the same `PaseoApi`: projects,
workspaces, agents, providers, and daemon config. They do not expose connection lifecycle. A surface borrows the
selected host's existing connection; switching the screen's host changes both `usePaseo()` and
`useRpc()` to that host. An offline selected host fails there and never falls through to another
installation. A server handler owns an IPC-backed daemon session for the life of its subprocess.
+74 -1
View File
@@ -63,6 +63,7 @@ function parseSentSessionMessage(data: string | ArrayBuffer | Uint8Array | undef
draftConfig?: unknown;
filter?: unknown;
page?: unknown;
sync?: unknown;
text?: string;
} {
if (typeof data !== "string") {
@@ -224,12 +225,84 @@ test("createPaseoApi borrows daemon capabilities without exposing connection own
const paseo = createPaseoApi(daemonClient);
expect(Object.keys(paseo).sort()).toEqual(["agents", "config", "providers", "workspaces"]);
expect(Object.keys(paseo).sort()).toEqual([
"agents",
"config",
"projects",
"providers",
"workspaces",
]);
expect("connect" in paseo).toBe(false);
expect("close" in paseo).toBe(false);
expect("skills" in paseo.agents).toBe(false);
});
test("project actions list registered projects through the existing RPC", async () => {
const { client, ws } = await connectClient();
const listPromise = client.projects.list({
requestId: "projects-list-request",
sync: { generation: "daemon-generation", afterSeq: 7 },
});
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
type: "project.list.request",
requestId: "projects-list-request",
sync: { generation: "daemon-generation", afterSeq: 7 },
});
ws.message(
sessionMessage({
type: "project.list.response",
payload: {
requestId: "projects-list-request",
projects: [
{
projectId: "project_sdk",
projectKey: "sdk",
projectDisplayName: "SDK",
projectCustomName: null,
projectCustomIconRevision: null,
projectIconRevision: "icon-revision",
projectRootPath: "/repo/sdk",
projectKind: "git",
syncSeq: 8,
},
],
sync: {
generation: "daemon-generation",
headSeq: 8,
mode: "changes",
removals: [],
},
},
}),
);
await expect(listPromise).resolves.toEqual({
requestId: "projects-list-request",
projects: [
{
projectId: "project_sdk",
projectKey: "sdk",
projectDisplayName: "SDK",
projectCustomName: null,
projectCustomIconRevision: null,
projectIconRevision: "icon-revision",
projectRootPath: "/repo/sdk",
projectKind: "git",
syncSeq: 8,
},
],
sync: {
generation: "daemon-generation",
headSeq: 8,
mode: "changes",
removals: [],
},
});
await client.close();
});
test("agent actions list the daemon directory without exposing the low-level client", async () => {
const { client, ws } = await connectClient();
const listedAgent = createAgent({ title: "Planner" });
+16
View File
@@ -8,11 +8,14 @@ import type {
ListProviderFeaturesRequestMessage,
ListProviderFeaturesResponseMessage,
ListProviderModelsResponseMessage,
ProjectListRequestMessage,
ProjectListResponseMessage,
ListProviderModesResponseMessage,
MutableDaemonConfig,
MutableDaemonConfigPatch,
ProviderDiagnosticResponseMessage,
ProjectPlacementPayload,
WorkspaceProjectDescriptorPayload,
RefreshProvidersSnapshotResponseMessage,
SendAgentMessageRequest,
SessionOutboundMessage,
@@ -77,6 +80,11 @@ export interface PaseoClientConfig {
export type PaseoWorkspace = WorkspaceDescriptorPayload;
export type PaseoAgent = AgentSnapshotPayload;
export type PaseoAgentListOptions = FetchAgentsOptions;
export type PaseoProject = WorkspaceProjectDescriptorPayload;
export type PaseoProjectListOptions = Omit<ProjectListRequestMessage, "type" | "requestId"> & {
requestId?: string;
};
export type PaseoProjectListResult = ProjectListResponseMessage["payload"];
export interface PaseoAgentListResult {
requestId: string;
@@ -143,6 +151,10 @@ export interface PaseoWorkspaceHandle {
subscribe(handler: (update: PaseoWorkspaceUpdate) => void): () => void;
}
export interface PaseoProjectActions {
list(options?: PaseoProjectListOptions): Promise<PaseoProjectListResult>;
}
export interface PaseoWorkspaceActions {
list(options?: PaseoWorkspaceListOptions): Promise<PaseoWorkspaceListResult>;
ref(workspace: string | PaseoWorkspace): PaseoWorkspaceHandle;
@@ -356,6 +368,7 @@ export interface PaseoConfigActions {
export interface PaseoApi {
readonly workspaces: PaseoWorkspaceActions;
readonly projects: PaseoProjectActions;
readonly agents: PaseoAgentActions;
readonly providers: PaseoProviderActions;
readonly config: PaseoConfigActions;
@@ -412,6 +425,9 @@ export function createPaseoApi(daemonClient: DaemonClient): PaseoApi {
const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient, createAgent);
return {
projects: {
list: (options) => daemonClient.listProjects(options),
},
workspaces: {
list: (options) => daemonClient.fetchWorkspaces(options),
ref: (workspace) => createWorkspaceHandle(workspace),
+1 -1
View File
@@ -445,7 +445,7 @@ function PullRequestAction() {
}
```
The returned API covers workspaces, agents, 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.
The returned API covers projects, workspaces, agents, 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.
## Add plugin-specific backend behavior
+6
View File
@@ -95,6 +95,12 @@ Creation options include `config`, `cwd`, `parent`, `title`, `prompt`, `env`, `o
`agent.timeline.subscribe(handler)` listens for stream events belonging to the agent and returns a local unsubscribe function.
## `client.projects`
| Method | Result | Behavior |
| ---------------- | ------------------------ | ----------------------------------------------------------------------------- |
| `list(options?)` | `PaseoProjectListResult` | Lists every registered project, including projects with no active workspaces. |
## `client.workspaces`
| Method | Result | Behavior |