【免费下载链接】paseo
Orchestrate multiple coding agents from desktop and mobile
本文以仓库中的可运行示例插件plugin-examples/buttons为骨架,系统讲解 Paseo 插件如何向 Agent 工作区头部(Header)与消息输入区(Composer)贡献交互按钮,涵盖按钮描述符(Button Descriptor)的完整字段、action/menu/popover三种行为模式、update()/remove()动态生命周期、跨平台紧凑布局适配,以及从旧版 Pill 形状迁移与浏览器回归测试的验证方法。读完本文,你将能独立为 Paseo 插件实现可实时刷新、可隐藏、可禁用并随 Agent 切换迁移的头部按钮与 Composer Pill。
示例概览:一个插件,五种命令,两类表面
plugin-examples/buttons是一个最小但功能完整的客户端插件:它不包含index.server.ts服务端入口,所有能力都来自客户端运行时。安装该目录为插件、打开任意 Agent 后,在 Command Center 中依次选择以下命令即可观察同一份注册在不同模式下的表现:
| Command Center 命令 | Header 表现 | Composer 表现 | 可以尝试什么 |
|---|---|---|---|
| Button examples: action | 仅 Refresh 图标 | Refresh 图标 + 标签 | 点击 Refresh 会从 daemon 读取 workspace 并刷新标签与 tooltip;pending 状态与错误反馈由 Paseo 负责 |
| Button examples: menu | Wrench、Tools、chevron | Wrench 与 Tools | 菜单内含刷新、分隔线、自定义详情项、Display 子菜单以及一个禁用项 |
| Button examples: popover | 响应式状态圆点、Details、chevron | 响应式状态圆点与 Details | 通过useWorkspace实时接收 workspace 数据;点击 Done 关闭表面 |
| Button examples: hide / show | 隐藏或恢复按钮 | 隐藏或恢复 pill | 可见性通过registration.update({ visible })切换 |
| Button examples: disable / enable | 禁用或启用按钮 | 禁用或启用 pill | 禁用状态通过registration.update({ disabled })切换 |
以下三张截图来自示例插件的screenshots目录,分别展示了 action 模式的头部按钮与 Composer Pill、命名 Tools 菜单、以及自定义状态图标与 popover:
示例每次只贡献一个头部按钮:其余附加动作收纳在命名按钮Tools的菜单之下。示例刻意不添加三点(more)控件,也不触发插件溢出菜单,保证回归测试可以精确断言每个能力都直接呈现在界面上。
插件清单与安装前提
插件目录结构如下(来自 plugin-examples/buttons):
buttons/ paseo-plugin.json # 插件清单 index.client.tsx # 客户端运行时入口 client/examples.tsx # 按钮描述符、更新逻辑与 React 内容 screenshots/ # 各模式的界面截图清单文件 paseo-plugin.json 声明了插件 ID 与最低 Paseo 版本要求:
{ "id": "button-examples", "requirements": { "paseo": ">=0.8.0" } }根据 插件参考文档 中的说明,requirements.paseo接受 npm semver 范围:>=0.8.0表示兼容 0.8.0 及之后(含预发布与未来破坏性版本)的发行版;缺省该字段则等价于<0.8.0,Paseo 0.8 及之后会拒绝加载并给出指向迁移指南的提示。因此本示例要求 Paseo 0.8 及以上版本。
按钮描述符:一个描述符驱动两种表面
Header 按钮与 Composer Pill 共用同一个按钮描述符(Button Descriptor),类型与字段均从@getpaseo/plugin/client导出。这是本示例的核心设计:createButtonExamples中的button(next)函数只构造一份描述符,composerButton(next)再通过展开运算符叠加 Composer 侧专属的展示信息(见 client/examples.tsx)。
描述符完整字段如下(摘自 按钮描述符章节):
| 字段 | 必填 | 含义 |
|---|---|---|
title | 是 | 非空的无障碍标签、tooltip 与 sheet 标题 |
icon | 是 | Lucide 图标名,或ComponentType<PluginButtonIconProps>自定义图标组件 |
label | 否 | 非空展示文本;省略则使用所在位置的默认值 |
visible | 否 | 默认为true;为false时移除触发器及其占位空间 |
disabled | 否 | 默认为false;按钮保持可见但不可交互 |
behavior | 是 | 三种行为形状之一(见下) |
行为(behavior)是一个判别联合类型:
type PluginButtonBehavior = | { kind: "action"; onPress(): void | Promise<void> } | { kind: "menu"; items: readonly PluginButtonMenuEntry[] } | { kind: "popover"; Content: React.ComponentType<PluginButtonContentProps> };- action:在客户端执行。Paseo 会在 promise 结算前把按钮标记为 busy、阻止重复点击,并用 toast 展示失败信息,失败后可重试。
- menu:弹出菜单。宽布局下为锚定浮层,紧凑布局下为底部 sheet。
- popover:渲染自定义 React Native 内容。Paseo 负责锚定、滚动、内边距与 sheet 呈现,插件只提供内容主体。
两种注册方式都返回{ update, remove }句柄(这是 Header 按钮与 Composer Pill 区别于其他客户端注册的特例——其余注册返回幂等移除函数)。示例中header与pill分别通过client.addHeaderButton与client.addComposerPill注册(examples.tsx L181-L187):
const header = client.addHeaderButton({ id: "example", workspaceId, button: button(mode) }); const pill = client.addComposerPill({ id: "example", workspaceId, agentId, button: composerButton(mode), });注意这里id在同一个目标(workspace / agent)内保持插件局部唯一:同一个 ID 可以用于不同目标或不同位置,但在同一目标内重复注册会抛错(见 更新与生命周期)。
Header 按钮与 Composer Pill 的差异
两者共享描述符,但呈现规则不同(参考文档):
- Header 按钮:
client.addHeaderButton({ id, workspaceId, button })将按钮加在工作区头部右侧的内置动作之前。省略label即为纯图标按钮;菜单与 popover 在宽布局下显示 chevron。紧凑布局下头部按钮使用纯图标、无标签无 chevron,且为无边框样式。 - Composer Pill:
client.addComposerPill({ id, workspaceId, agentId, button })将 pill 放在指定 Agent 的 Composer 轨道上(与 Tasks、Subagents 并列)。Pill始终显示图标与label(省略 label 时回退到title),并且从不显示 chevron——即使行为是菜单或 popover 也一样。紧凑布局下 pill 依旧保留图标与标签。
示例通过composerPresentation表(examples.tsx L13-L17)为三种模式提供 Composer 专属的标题与标签,实现同一行为在两种表面上的差异化呈现:
const composerPresentation = { action: { title: "Refresh context", label: "Refresh" }, menu: { title: "Composer tools", label: "Tools" }, popover: { title: "Context details", label: "Details" }, };三种行为模式的源码拆解
action:把真实操作交给 Paseo 管理
action 模式的核心是refreshWorkspace(examples.tsx L80-L88)。它返回真实操作的 Promise,让 Paseo 接管 pending 状态、防重复点击与错误反馈:
async function refreshWorkspace() { // 返回真实操作的 promise:Paseo 负责 pending、防重复点击与错误处理。 await client.paseo.workspaces.ref(workspaceId).refresh(); refreshes += 1; if (mode === "action") { header.update({ title: `Refresh workspace (${refreshes})` }); pill.update({ label: `Refreshed · ${refreshes}` }); } }刷新成功后,update()把刷新计数写回标题与标签——这是「刷新后更新 UI」的标准姿势:不要直接修改原始描述符对象,而要调用update(patch: Partial<PluginButton>)原地变更描述符并保留身份与顺序。button("action")分支返回的描述符如下(L90-L97):
if (next === "action") return { title: "Refresh workspace", icon: "RefreshCw", label: undefined, // 头部为纯图标;Composer 用自己的 label behavior: { kind: "action", onPress: refreshWorkspace }, };注释点明了label: undefined的意图:Header 侧保持纯图标,而 Composer 侧通过composerPresentation提供label: "Refresh"。
menu:分隔线、子菜单与禁用项
button("menu")返回一个包含五类条目的菜单(L105-L171),完整覆盖菜单条目的三种用法:
- 普通 action 项:
refresh,图标RefreshCw,复用同一个refreshWorkspace; - 分隔线:
{ kind: "separator", id: "details-divider" },只含kind与id两个字段; - popover 项:
details,使用自定义图标组件WorkspaceStatusIcon,行为为 popover; - 嵌套子菜单:
display(图标Eye),其items内包含「Hide example buttons」与「Disable example buttons」两个 action 项——分别调用setVisible(false)与setDisabled(true); - 禁用项:
publish,带disabled: true,标题明确标注 "(unavailable in example)"。
菜单条目的 ID 规则与按钮 ID 一致:小写字母开头,仅含小写字母、数字与连字符,且在菜单内唯一。宽布局下嵌套菜单以 flyout 展开,紧凑布局下在同一 sheet 内以带返回导航的页面展开。选中 action 会关闭菜单;打开另一页面则保持菜单开启。Paseo 会在过滤隐藏项后自动移除前导、尾随与连续的分隔线。
popover:自定义图标 + 实时 workspace 数据
popover 模式展示了两个自定义能力:自定义图标组件与自定义内容组件。
WorkspaceStatusIcon是一个PluginButtonIconProps型组件(L19-L30),用useWorkspace订阅 workspace 状态,再依据状态映射为颜色圆点:
function WorkspaceStatusIcon({ workspaceId, size, color, theme }: PluginButtonIconProps) { const status = useWorkspace(workspaceId, (workspace) => workspace.status); let fill = color; if (status === "failed") fill = theme.colors.statusDanger; else if (status === "needs_input") fill = theme.colors.statusWarning; else if (status === "done") fill = theme.colors.statusSuccess; const style = useMemo( () => ({ width: size, height: size, borderRadius: size / 2, backgroundColor: fill }), [size, fill], ); return <View style={style} />; }PluginButtonIconProps提供theme、host、layout、size、color及目标上下文;组件必须在给定的size内渲染,指针交互全部由 Paseo 接管,且图标组件可以使用插件 hooks。
WorkspaceDetails是PluginButtonContentProps型内容组件(L32-L70),通过useWorkspace一次订阅多个字段,展示 workspace 名称、项目名、状态与 diff 统计:
const workspace = useWorkspace(workspaceId, ({ name, projectDisplayName, status, diffStat }) => ({ name, projectDisplayName, status, diffStat, }));内容区渲染一个Done按钮,其onPress={close}调用PluginButtonContentProps提供的close()来关闭表面。这正是 README 中「Done closes the surface」的实现:Paseo 拥有锚定、滚动、内边距与 sheet 呈现,插件只负责内容主体,并可在此使用usePaseo、useRpc、useWorkspace、useAgent以及安装实例的 React Query 缓存。
所有样式均取自theme.colors(foreground、foregroundMuted、surface2、statusDanger等),并遵循 跨平台规则:只用View/Text/Pressable,不使用任何 HTML 元素或 DOM 全局对象,以保证在 iOS、Android 与浏览器(React Native Web)中行为一致。
动态更新:可见性与禁用状态的切换
setVisible与setDisabled(examples.tsx L189-L196)通过update同步两个注册:
function setVisible(visible: boolean) { header.update({ visible }); pill.update({ visible }); } function setDisabled(disabled: boolean) { header.update({ disabled }); pill.update({ disabled }); }生命周期语义(更新与生命周期)值得强调:
update(patch)原地变更描述符,保留身份与顺序;变更behavior时必须提供完整的 behavior 对象;非法更新会抛错且不改变现有按钮。- 隐藏或禁用会关闭其已打开的表面;变更 behavior 同样关闭表面。隐藏保留注册,因此重新显示时恢复原位置,但不会取消进行中的 action。
remove()是幂等的;移除后的update不产生任何效果。插件卸载或宿主连接断开时,Paseo 会自动移除未清理的按钮。
切换 Agent / Workspace 与入口清理
client/examples.tsx是描述符与逻辑的载体,而入口文件 index.client.tsx 拥有活动示例的切换策略:它保持「同一时刻只有一个示例按钮组处于活动状态」,包括跨 Agent 与跨 Workspace 切换。
核心是buttonsFor函数(index.client.tsx L14-L24):当目标 workspace 或 agent 变化时,先对旧实例调用current.buttons.remove()完成清理,再为新目标创建新实例;目标未变化则复用现有句柄。三个循环分别注册 3 个模式命令、2 个可见性命令与 2 个禁用命令(L26-L58),共 7 个 Command Center 项,全部使用context: "agent"与onSelect回调:
for (const mode of ["action", "menu", "popover"] satisfies ButtonMode[]) { client.addCommandCenterItem({ id: `show-${mode}`, title: `Button examples: ${mode}`, icon: "MousePointerClick", context: "agent", onSelect(context) { buttonsFor(context).setMode(mode); }, }); }入口函数最后返回清理函数(L60):
return () => current?.buttons.remove();这符合插件参考文档的入口约定:客户端入口默认导出一个接收PluginClientContext的contribute函数并返回清理逻辑;入口清理会在 Paseo 移除其余注册之前运行,因此插件应在此释放订阅、定时器与 socket 等自有资源。createButtonExamples返回的remove()(L205-L208)同时调用header.remove()与pill.remove(),确保两个表面的注册一并卸载。
从旧版迁移:Pill 形状与清理句柄的变化
README 特别提醒既有插件项目:plugin-examples/buttons面向 Paseo 0.8 的运行时入口格式。0.7 及更早版本的插件在迁移时需同步更新@getpaseo/plugin依赖,否则npm run typecheck会立即报出旧 API 的 TypeScript 错误。按 Composer Pill 迁移指南,变化要点如下:
| 旧形状 | 0.8 形状 |
|---|---|
Component渲染整个 pill | button.icon+button.label |
title作为独立字段 | 移入button.title |
onPress作为独立回调 | 移入button.behavior({ kind: "action", onPress }) |
| 注册返回值是函数,直接调用即清理 | 注册返回{ update, remove },调用.remove()清理 |
迁移后的典型写法:
const pill = client.addComposerPill({ id: "review", workspaceId, agentId, button: { title: "Open review", icon: "Scan", label: "Review", behavior: { kind: "action", onPress: openReview }, }, }); pill.update({ label: "Review · 3", visible: true }); // 客户端入口清理: pill.remove();PluginComposerPillProps不再导出,旧式按函数调用的注册方式会直接报 TypeScript 错误——这是机械迁移中最容易定位的信号。旧的动态文本需要从「组件内部渲染」改为「模型或 SDK 订阅后调用update推送」;自定义图标组件仍可使用 hooks。
类型检查与浏览器回归测试
README 给出两条验证命令。类型检查针对 SDK 契约:
npm run typecheck --workspace=@getpaseo/plugin它把示例代码与@getpaseo/pluginSDK 类型对齐;既有插件项目必须先升级依赖再做自己的npm run typecheck,才能发现旧的Component/onPresspill 形状与可调用的清理句柄。
浏览器回归测试把该目录原样安装进隔离的 daemon,在桌面与手机两种宽度下分别验证两种布局、动态更新与清理逻辑(plugin-button-example.spec.ts):
npm run test:e2e --workspace=@getpaseo/app -- e2e/browser/plugin-button-example.spec.ts测试依次执行三个断言组:refreshFromHeaderAndComposer(从头部与 Composer 触发刷新)、useMenusAndToggleButtons(使用菜单并切换可见性/禁用)、inspectLiveWorkspaceDetails(检查实时 workspace 详情),并在开头验证「没有任何竞争的溢出按钮」,这正是示例刻意不引入三点控件与插件溢出行为的原因。需要说明的边界是:截图基于 Chromium 的桌面与手机宽度生成,原生 iOS 与 Android 并未被该测试覆盖(README 明确声明了这一点)。
小结:把「按钮」当作可编程的注册而非静态 UI
plugin-examples/buttons示例的精髓在于:Header 按钮与 Composer Pill 只是同一份按钮描述符在两种表面上的呈现,而update/remove句柄赋予插件完整的动态控制力——实时刷新标签、切换行为模式、隐藏恢复、禁用启用、随 Agent 迁移。配合paseo-plugin.json的版本要求、客户端入口的清理约定、跨平台组件规则与浏览器回归测试,这个最小示例构成了从「写一个按钮」到「在生产插件中管理复杂按钮生命周期」的完整参考路径。想继续深入,可对照 插件参考文档 中的按钮描述符、菜单条目与生命周期契约,或在 迁移指南 中查看从旧版 Pill 形状升级的完整核对表。
【免费下载链接】paseo
Orchestrate multiple coding agents from desktop and mobile
相关推荐
如何打造专属MacBook Pro触控栏:从静态按钮到动态脚本按钮的完整指南
如何打造专属MacBook Pro触控栏:从静态按钮到动态脚本按钮的完整指南 MTMR(My TouchBar My Rules)是一款强大的MacBook P
桌面应用Epic Stack按钮组件:交互按钮实现
Epic Stack按钮组件:交互按钮实现 概述 Epic Stack的按钮组件系统基于现代React技术栈构建,提供了高度可定制化的交互按钮解决方案。该系统包
后端前端开发工具认证鉴权如何快速实现React Native Navigation悬浮按钮:FAB按钮的完整指南
如何快速实现React Native Navigation悬浮按钮:FAB按钮的完整指南 React Native Navigation是一个功能强大的原生导航
移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考