feat(plugins): composer items and controls in all three composers

A plugin registers a component with addComposerItem and reads and changes the
composer through useComposer(): provider, model, mode, thinking, options,
prompt, attachments and agent metadata. The agent, draft and New workspace
composers provide the same ComposerInput and ComposerAgent halves, so a
command reaches the right model without the plugin knowing which composer
it sits in.

Toggle, Select and Action are drawn by one ComposerControl renderer that
the thinking select and provider option controls now use too. Controls sit in
the toolbar when there is room and become rows in the existing overflow sheet
otherwise, so on a phone they appear in the overflow. Plugin items count in
the density presence so the composer condenses to make room. Model and mode
stay specialized but share the trigger, density and overflow.

Draft metadata is kept locally, sent at creation and reset when the prompt is
sent. PluginAgentSnapshot.metadata, deferred from the metadata phase, lands
here. The unused provider badge is removed.

vitest resolves @floating-ui/react-native to its ESM build: the CJS entry
requires the real react-native, whose Flow sources do not parse.
This commit is contained in:
Mohamed Boudra
2026-11-02 05:36:04 +01:00
parent 77f90d3460
commit abd4d2c1ea
57 changed files with 2029 additions and 783 deletions
+1
View File
@@ -14,6 +14,7 @@ A plugin is a TypeScript project installed into one Paseo daemon. It can add
[file panels](/docs/plugins/reference#file-panels),
[Command Center items](/docs/plugins/reference#command-center-items),
[slash commands](/docs/plugins/reference#slash-commands),
[composer items](/docs/plugins/reference#composer-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-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)、[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 上安装一个:
+73
View File
@@ -1894,6 +1894,79 @@ header has no room for plugin buttons, so each one is a row of the workspace men
icon and title; its menus and popovers open as pages of that sheet. Placement and overflow are host
decisions.
## Composer items
`client.addComposerItem({ id, Component })` puts a component in the composer: the agent composer,
the draft composer for a new agent, and the composer on the New workspace screen. `useComposer()`
reads and changes what the composer configures, and `Toggle`, `Select` and `Action` from
`@getpaseo/plugin/client/ui` draw the controls.
```tsx
import { useComposer, type PluginClientContext } from "@getpaseo/plugin/client";
import { Action, Select, Toggle } from "@getpaseo/plugin/client/ui";
function ReviewFlag() {
const composer = useComposer();
return (
<Toggle
title="Needs review"
icon="Flag"
value={composer.agent.metadata["review.needsReview"] === true}
onValueChange={(next) => composer.setMetadata({ "review.needsReview": next ? true : null })}
/>
);
}
function Checklist() {
const composer = useComposer();
return (
<Action
title="Insert checklist"
icon="ListChecks"
onPress={() => composer.insertText("- [ ] Tests\n- [ ] Docs\n")}
/>
);
}
export default function contribute(client: PluginClientContext) {
client.addComposerItem({ id: "review-flag", Component: ReviewFlag });
client.addComposerItem({ id: "checklist", Component: Checklist });
return () => {};
}
```
`useComposer()` returns the composer the item sits in:
| Field or command | Meaning |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `context` | `"agent"` for a running agent, `"draft"` for a new agent in a workspace, `"new-workspace"` for the New workspace screen. |
| `prompt`, `setPrompt(text)`, `insertText(text)` | The prompt. `insertText` replaces the selection or inserts at the cursor. |
| `attachments`, `addAttachment`, `removeAttachment` | Items from [your own attachment sources](#add-a-composer-attachment-source). Attachments from other plugins and Paseo's own are not listed. |
| `agent.provider`, `model`, `mode`, `thinking` | What the agent runs with, or what the draft will create. |
| `agent.options` | The provider options in effect, defaults included. |
| `agent.metadata` | The agent's [metadata](/docs/plugins/hooks#metadata). In New workspace, the first agent's. |
| `setProvider`, `setModel`, `setMode`, `setThinking` | Return promises. `setProvider` resets the model like the provider picker, and rejects on `context: "agent"`: a running agent keeps its provider. |
| `setOptions(patch)`, `setMetadata(patch)` | Top-level keys replace; `null` removes the key. Metadata set in a draft is sent when the agent is created and cleared once the prompt is sent. |
The same call does the same thing in every composer; a plugin does not choose a composer's
implementation. `setMetadata` on a running agent needs a host that supports metadata. On an older
host the promise rejects with "Update the host to use metadata."
`Toggle` takes `value` and `onValueChange`. `Select` takes `value`, `options` (`{ value, label,
description? }`) and `onValueChange`. `Action` takes `onPress`. All three take `title`, `icon` (a
[Lucide name, SVG or component](#icons)) and `disabled`. Paseo owns where each control sits: inline
in the toolbar when the composer has room, and a row in the composer's overflow sheet when it does
not, which is always the case on a phone. A `Select` opens its list in place of the sheet row. A
`Component` that renders something other than these controls draws it where its item is placed; in
the overflow sheet that is the sheet.
A composer item counts toward how much fits before Paseo knows what it renders. An item that
renders nothing can still leave an empty overflow sheet on a narrow composer, so always render a
control.
The [composer controls example](https://github.com/getpaseo/paseo/tree/main/plugin-examples/composer-controls)
registers a `Select`, a `Toggle` and an `Action`.
## Composer pills
`client.addComposerPill({ id, workspaceId, agentId, button })` uses the same [button descriptor](#button-descriptor)
+59
View File
@@ -1600,6 +1600,65 @@ review.remove();
贡献移入一个共享的溢出菜单。在手机和较窄的窗口中,标题栏没有空间放置插件按钮,因此每个按钮都会成为工作区
菜单(`···`)中的一行,显示它的图标和标题;它的菜单和弹出框以该底部面板中的页面打开。位置和溢出由宿主决定。
## Composer 项 {#composer-items}
`client.addComposerItem({ id, Component })` 把一个组件放进 composer:Agent composer、为新 Agent 准备的草稿 composer,以及新建工作区界面上的 composer。`useComposer()` 读取并修改 composer 所配置的内容,`@getpaseo/plugin/client/ui` 中的 `Toggle`、`Select` 和 `Action` 负责绘制控件。
```tsx
import { useComposer, type PluginClientContext } from "@getpaseo/plugin/client";
import { Action, Select, Toggle } from "@getpaseo/plugin/client/ui";
function ReviewFlag() {
const composer = useComposer();
return (
<Toggle
title="Needs review"
icon="Flag"
value={composer.agent.metadata["review.needsReview"] === true}
onValueChange={(next) => composer.setMetadata({ "review.needsReview": next ? true : null })}
/>
);
}
function Checklist() {
const composer = useComposer();
return (
<Action
title="Insert checklist"
icon="ListChecks"
onPress={() => composer.insertText("- [ ] Tests\n- [ ] Docs\n")}
/>
);
}
export default function contribute(client: PluginClientContext) {
client.addComposerItem({ id: "review-flag", Component: ReviewFlag });
client.addComposerItem({ id: "checklist", Component: Checklist });
return () => {};
}
```
`useComposer()` 返回该项所在的 composer:
| 字段或命令 | 含义 |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `context` | `"agent"` 表示运行中的 Agent,`"draft"` 表示工作区中的新 Agent,`"new-workspace"` 表示新建工作区界面。 |
| `prompt`、`setPrompt(text)`、`insertText(text)` | 提示词。`insertText` 会替换选区,或在光标处插入。 |
| `attachments`、`addAttachment`、`removeAttachment` | 来自[你自己的附件来源](#add-a-composer-attachment-source)的项目。其他插件和 Paseo 自身的附件不会列出。 |
| `agent.provider`、`model`、`mode`、`thinking` | Agent 当前使用的配置,或草稿将要创建的配置。 |
| `agent.options` | 当前生效的 Provider 选项,包含默认值。 |
| `agent.metadata` | Agent 的[元数据](/docs/plugins/hooks#metadata)。在新建工作区中是第一个 Agent 的元数据。 |
| `setProvider`、`setModel`、`setMode`、`setThinking` | 返回 Promise。`setProvider` 会像 Provider 选择器一样重置模型,在 `context: "agent"` 下会被拒绝:运行中的 Agent 保持其 Provider。 |
| `setOptions(patch)`、`setMetadata(patch)` | 顶层键替换,`null` 删除该键。草稿中设置的元数据在创建 Agent 时发送,发送提示词后清空。 |
同一个调用在每个 composer 中行为一致,插件不需要选择 composer 的实现。在运行中的 Agent 上调用 `setMetadata` 需要支持元数据的主机;主机过旧时,Promise 会以 “Update the host to use metadata.” 拒绝。
`Toggle` 接收 `value` 和 `onValueChange`。`Select` 接收 `value`、`options`(`{ value, label, description? }`)和 `onValueChange`。`Action` 接收 `onPress`。三者都接收 `title`、`icon`([Lucide 名称、SVG 或组件](#icons))和 `disabled`。控件的位置由 Paseo 决定:composer 空间足够时显示在工具栏中,不够时(手机上始终如此)成为 composer 溢出面板中的一行。`Select` 会就地打开列表,而不是替换面板中的行。`Component` 如果渲染这些控件以外的内容,会在其所在位置绘制;在溢出面板中,该位置就是面板。
在 Paseo 知道渲染结果之前,composer 项就已计入可容纳的数量。不渲染任何内容的项仍可能在窄 composer 上留下空的溢出面板,所以请始终渲染一个控件。
[Composer 控件示例](https://github.com/getpaseo/paseo/tree/main/plugin-examples/composer-controls)注册了一个 `Select`、一个 `Toggle` 和一个 `Action`。
## Composer 胶囊 {#composer-pills}
`client.addComposerPill({ id, workspaceId, agentId, button })` 使用同样的[按钮描述符](#button-descriptor),