feat(plugins): New workspace items set the branch and workspace metadata

addNewWorkspaceItem mounts a plugin field on the New workspace screen, and
useNewWorkspace() exposes host, project, isolation, base ref, branch and
metadata with commands that follow the composer item rules: they act on the
screen's state when they run, resolve once applied, reject while the screen
is locked, and take JSON payloads. Host and project are read-only because a
command that switched them would unmount the item that called it.

The screen's pickable state (base selection, manual isolation, branch,
metadata) moves into one plain store so pickers and plugin commands write to
the same place; the screen reports derived facts to it on each render. The
branch is sent as source.branchName only for a branch-off worktree, and
metadata on workspace.create.

Items render through ComposerControl with a new "field" surface (badge on
desktop, full-width field when compact). Combobox ships here: Paseo owns the
loading and error states, keeps previous results, and drops superseded
searches.
This commit is contained in:
Mohamed Boudra
2026-11-04 07:25:21 +01:00
parent d5e022e363
commit 14fc9b02f8
55 changed files with 1517 additions and 106 deletions
+1
View File
@@ -15,6 +15,7 @@ A plugin is a TypeScript project installed into one Paseo daemon. It can add
[Command Center items](/docs/plugins/reference#command-center-items),
[slash commands](/docs/plugins/reference#slash-commands),
[composer items](/docs/plugins/reference#composer-items),
[New workspace items](/docs/plugins/reference#new-workspace-items),
[composer pills](/docs/plugins/reference#composer-pills),
[timeline items](/docs/plugins/reference#timeline-items),
[themes](/docs/plugins/reference#contribute-a-theme),
+1 -1
View File
@@ -6,7 +6,7 @@ nav: 快速上手
# 插件快速上手 {#plugin-quickstart}
插件是安装到某个 Paseo daemon 中的 TypeScript 项目。它可以添加[界面和侧边栏项](/docs/plugins/reference#screens-and-sidebar-items)、[工作区面板](/docs/plugins/reference#workspace-panels)、[文件面板](/docs/plugins/reference#file-panels)、[Command Center 项](/docs/plugins/reference#command-center-items)、[斜杠命令](/docs/plugins/reference#slash-commands)、[Composer 项](/docs/plugins/reference#composer-items)、[Composer 标签](/docs/plugins/reference#composer-pills)、[时间线条目](/docs/plugins/reference#timeline-items)、[主题](/docs/plugins/reference#contribute-a-theme)、[附件来源](/docs/plugins/reference#add-a-composer-attachment-source)和 [daemon 端 RPC](/docs/plugins/reference#add-plugin-specific-backend-behavior)。它还可以[把编程 Agent 接入为 Provider](/docs/plugins/providers)。客户端贡献会在连接到该 daemon 的每个 Paseo 客户端上运行,包括移动端。
插件是安装到某个 Paseo daemon 中的 TypeScript 项目。它可以添加[界面和侧边栏项](/docs/plugins/reference#screens-and-sidebar-items)、[工作区面板](/docs/plugins/reference#workspace-panels)、[文件面板](/docs/plugins/reference#file-panels)、[Command Center 项](/docs/plugins/reference#command-center-items)、[斜杠命令](/docs/plugins/reference#slash-commands)、[Composer 项](/docs/plugins/reference#composer-items)、[新建工作区项](/docs/plugins/reference#new-workspace-items)、[Composer 标签](/docs/plugins/reference#composer-pills)、[时间线条目](/docs/plugins/reference#timeline-items)、[主题](/docs/plugins/reference#contribute-a-theme)、[附件来源](/docs/plugins/reference#add-a-composer-attachment-source)和 [daemon 端 RPC](/docs/plugins/reference#add-plugin-specific-backend-behavior)。它还可以[把编程 Agent 接入为 Provider](/docs/plugins/providers)。客户端贡献会在连接到该 daemon 的每个 Paseo 客户端上运行,包括移动端。
浏览[已发布的插件](https://paseo.sh/plugins),然后在你的 daemon 上安装一个:
+71
View File
@@ -1984,6 +1984,77 @@ control.
The [composer controls example](https://github.com/getpaseo/paseo/tree/main/plugin-examples/composer-controls)
registers a `Select`, two `Toggle`s (one writes a provider option) and an `Action`.
## New workspace items
`client.addNewWorkspaceItem({ id, Component })` puts a field on the New workspace screen, beside
the project, host and branch pickers. `useNewWorkspace()` reads and changes what the screen will
create, and `Combobox`, `Select`, `Toggle` and `Action` from `@getpaseo/plugin/client/ui` draw the
fields. On desktop an item is a badge in the setup row; on a phone it is a full-width field.
```tsx
import { useNewWorkspace, useRpc, type PluginClientContext } from "@getpaseo/plugin/client";
import { Combobox } from "@getpaseo/plugin/client/ui";
import { searchIssuesRpc } from "../shared/issues";
function IssueField() {
const newWorkspace = useNewWorkspace();
const searchIssues = useRpc(searchIssuesRpc);
const issueId = newWorkspace.metadata["issues.id"];
return (
<Combobox
title="Issue"
icon="CircleDot"
value={typeof issueId === "string" ? { id: issueId, label: issueId } : null}
search={async ({ query }) => (await searchIssues({ query })).issues}
onSelect={async ({ item }) => {
await newWorkspace.setBranch(item.branch);
await newWorkspace.setMetadata({ "issues.id": item.id });
}}
/>
);
}
export default function contribute(client: PluginClientContext) {
client.addNewWorkspaceItem({ id: "issue", Component: IssueField });
return () => {};
}
```
`useNewWorkspace()` returns the setup the item sits in:
| Field or command | Meaning |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host`, `project` | The user's choices, `{ id, label }` and `{ id, name } \| null`. Read-only: an item cannot switch host or project. |
| `isolation`, `setIsolation` | `"local"` or `"worktree"`. A project that cannot make worktrees reads `"local"`, and `setIsolation("worktree")` rejects. |
| `baseRef`, `setBaseRef` | What a new worktree starts from: `{ type: "branch", name }`, `{ type: "change-request", number }`, or `null` until the default is known. `setBaseRef({ type: "branch", name })` rejects while no project is chosen. |
| `branch`, `setBranch` | The name of the branch a new worktree is created on. `null` lets Paseo pick one. It is kept while isolation is local or the base is a change request, and it applies only to a worktree created from a branch. |
| `metadata`, `setMetadata` | The created workspace's [metadata](/docs/plugins/hooks#metadata), not the first agent's. Top-level keys replace; `null` removes a key. Rejects on a host that does not support metadata with "Update the host to use metadata." |
Commands follow the [composer item](#composer-items) rules: each one acts on the screen's state
when it runs, so `await newWorkspace.setBranch(name)` followed by `newWorkspace.setMetadata(patch)`
changes both, and its promise resolves once the change has taken effect. Payloads are JSON values.
`metadata` is a frozen copy. While the screen creates the workspace it is locked: your fields render
disabled and every command rejects with "The New workspace screen is locked while the workspace is
being created." A callback that returns a promise keeps the control disabled until it settles, and a
rejection reaches the user as an error toast.
`Combobox` is a field with a searchable list whose results come from your code:
| Prop | Meaning |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` | `{ id, label, description? } \| null`, what the field shows. It need not be among the search results, so it can show a choice made earlier. |
| `search` | `({ query }) => Promise<Item[]>`. Called with `""` when the list opens and again, debounced, as the user types. Each `Item` is `{ id, label, description? }` plus any fields you add. |
| `onSelect` | `({ item }) => void \| Promise<void>`. `item` is the object `search` returned. |
Paseo shows "Searching…" while a search runs, "Search failed" with the error message when it
rejects, and an empty state for no results. It keeps the previous results while a newer search runs
and ignores a search that a newer one replaced, so `search` needs no cancellation of its own.
`Combobox` takes the same `title`, `icon` and `disabled` as the other controls, and draws as a row
with a list in the composer's overflow sheet too.
The [New workspace issues example](https://github.com/getpaseo/paseo/tree/main/plugin-examples/new-workspace-issues)
searches a fixed issue list from a daemon handler and sets the branch and metadata from the choice.
## Composer pills
`client.addComposerPill({ id, workspaceId, agentId, button })` uses the same [button descriptor](#button-descriptor)
+57
View File
@@ -1665,6 +1665,63 @@ export default function contribute(client: PluginClientContext) {
[Composer 控件示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/composer-controls)注册了一个 `Select`、两个 `Toggle`(其中一个写入 Provider 选项)和一个 `Action`。
## 新建工作区项 {#new-workspace-items}
`client.addNewWorkspaceItem({ id, Component })` 在新建工作区界面上放置一个字段,位于项目、主机和分支选择器旁边。`useNewWorkspace()` 读取并修改界面将要创建的内容,`@getpaseo/plugin/client/ui` 中的 `Combobox`、`Select`、`Toggle` 和 `Action` 负责绘制字段。桌面端的项是设置行中的徽章;手机上是整行宽度的字段。
```tsx
import { useNewWorkspace, useRpc, type PluginClientContext } from "@getpaseo/plugin/client";
import { Combobox } from "@getpaseo/plugin/client/ui";
import { searchIssuesRpc } from "../shared/issues";
function IssueField() {
const newWorkspace = useNewWorkspace();
const searchIssues = useRpc(searchIssuesRpc);
const issueId = newWorkspace.metadata["issues.id"];
return (
<Combobox
title="Issue"
icon="CircleDot"
value={typeof issueId === "string" ? { id: issueId, label: issueId } : null}
search={async ({ query }) => (await searchIssues({ query })).issues}
onSelect={async ({ item }) => {
await newWorkspace.setBranch(item.branch);
await newWorkspace.setMetadata({ "issues.id": item.id });
}}
/>
);
}
export default function contribute(client: PluginClientContext) {
client.addNewWorkspaceItem({ id: "issue", Component: IssueField });
return () => {};
}
```
`useNewWorkspace()` 返回该项所在的设置:
| 字段或命令 | 含义 |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `host`、`project` | 用户的选择,分别是 `{ id, label }` 和 `{ id, name } \| null`。只读:项不能切换主机或项目。 |
| `isolation`、`setIsolation` | `"local"` 或 `"worktree"`。无法创建 worktree 的项目读取为 `"local"`,此时 `setIsolation("worktree")` 会被拒绝。 |
| `baseRef`、`setBaseRef` | 新 worktree 的起点:`{ type: "branch", name }`、`{ type: "change-request", number }`,默认值未知时为 `null`。未选择项目时,`setBaseRef({ type: "branch", name })` 会被拒绝。 |
| `branch`、`setBranch` | 新 worktree 所创建的分支名称。`null` 表示由 Paseo 选择名称。隔离为本地或起点为变更请求时会保留该值,它只对从分支创建的 worktree 生效。 |
| `metadata`、`setMetadata` | 所创建工作区的[元数据](/docs/plugins/hooks#metadata),而不是第一个 Agent 的元数据。顶层键替换,`null` 删除该键。主机不支持元数据时以 “Update the host to use metadata.” 拒绝。 |
命令遵循 [Composer 项](#composer-items)的规则:每个命令都按界面运行时的状态执行,所以 `await newWorkspace.setBranch(name)` 之后再调用 `newWorkspace.setMetadata(patch)` 会同时生效,命令的 Promise 在更改生效后完成。载荷是 JSON 值。`metadata` 是冻结的副本。界面创建工作区期间会被锁定:你的字段显示为禁用,所有命令都会以 "The New workspace screen is locked while the workspace is being created." 拒绝。返回 Promise 的回调在完成之前会让控件保持禁用,被拒绝时会以错误提示向用户显示。
`Combobox` 是带可搜索列表的字段,结果由你的代码提供:
| 属性 | 含义 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` | `{ id, label, description? } \| null`,字段显示的内容。它不必出现在搜索结果中,因此可以显示之前所做的选择。 |
| `search` | `({ query }) => Promise<Item[]>`。列表打开时以 `""` 调用,之后随用户输入防抖调用。每个 `Item` 是 `{ id, label, description? }` 加上你添加的字段。 |
| `onSelect` | `({ item }) => void \| Promise<void>`。`item` 就是 `search` 返回的对象。 |
搜索进行时 Paseo 显示 “Searching…”,搜索被拒绝时显示 “Search failed” 和错误信息,没有结果时显示空状态。新搜索进行期间会保留之前的结果,并忽略已被更新搜索取代的结果,所以 `search` 不需要自己处理取消。`Combobox` 接收与其他控件相同的 `title`、`icon` 和 `disabled`。
[新建工作区 Issue 示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/new-workspace-issues)通过守护进程处理器搜索一个固定的 Issue 列表,并根据所选项设置分支和元数据。
## Composer 胶囊 {#composer-pills}
`client.addComposerPill({ id, workspaceId, agentId, button })` 使用同样的[按钮描述符](#button-descriptor),