* feat(website): serve paseo.sh in Simplified Chinese
Every page of the website now has a Simplified Chinese version under
/zh-CN: the interface, docs, blog posts, comparison pages, legal pages,
all changelog releases and the homepage mockup. English URLs are unchanged.
The URL alone decides the language. There is no browser-language redirect
and no stored preference, because either would have to redirect the
unprefixed English URLs that people share and search engines index.
Internal links carry the page's language instead, and hreflang alternates
point Chinese search results at /zh-CN.
Translated headings pin the English anchor with {#id}, so section links
work in both languages. content-translations.test.ts fails when a page
has no translation or a translation's headings drift from the English.
Changelog releases and plugin listings without a translation render
whole in English under a notice, so a release never waits on one.
Plugin manifests and the registry gain optional translations and
OVERVIEW.zh-CN.md fields; older clients and registries ignore them.
* feat(website): replace the native language select with an EN/ZH menu
The header showed an OS-native select whose popup did not match the site.
The trigger now shows the language abbreviated (EN, ZH) and opens a menu in
the app's dropdown style that names each language in itself, with its name
in the page language underneath. Rows are real links to the same page in
that language; arrows, Escape, outside presses and taps behave as a menu
button, and the menu keeps itself on screen when a narrow header pushes the
trigger toward an edge.
Also drops the stray space after the full-width comma in the Chinese
homepage subtitle.
* docs(website): translate main's new docs and 0.12.0 release notes
Main added Keep run workspaces to the schedules docs, rewrote plugin icons
around inline SVG, and promoted 0.12.0 with new entries. Bring the Chinese
pages and changelog in line so no section falls back to English.
* fix(server): leave plugin listing translations to the registry
The daemon rejected manifests whose translations the registry would refuse,
so a plugin main installs could fail to load here. Paseo never reads
translations; the registry validates them at publish. Docs now say which
checks are automated at publish and which are human review.
* docs(plugins): describe the registry's final overview translation discovery
* style(server): format manifest test
* test(app): measure sidebar rows one at a time
After a reload the developer error toast appears while expectVerticalOrder
measures every row at once. Each concurrent boundingBox runs the toast
locator handler, and only one of them resumes, so the poll timed out on
every attempt since #7424. Measuring sequentially keeps one action in flight.
* test(app): return sidebar row measuring to main
#7480 hides the developer toast instead of running a locator handler, so
measuring the rows concurrently no longer races it. Reverts 41ac23016.
* test(app): wait for the compact shell before opening compact Changes
The shell survives the breakpoint, so right after the resize the wide
Explorer could still be visible: the test skipped the Explorer toggle and
the tab then disappeared, or toggled at the wrong time. Wait for the
compact-shell marker, as the composer helpers do since #7424.
* docs(website): translate main's plugin file panel docs
* test(app): edit the code font size after the font family remount
A committed appearance change remounts the settings screen through
AppearanceStyleBoundary, and the new form starts from the stored size.
Filling the size before that remount lost the edit in CI.
* docs(website): translate main's client/ui import docs
Follows #7474: one /client/ui entry, StreamedText instead of useRevealedText,
SettingsGroup dropped from the settings table, and the new
'UI imports in v0.13' migration section.
* test(app): wait for the previous layout's file panel picker to leave
After a resize the old layout's picker animates out while the new one is
already mounted, so the Use button matched two elements and the strict
locator failed. Wait for the shell's layout and a single picker first.
17 KiB
title, description, nav
| title | description | nav |
|---|---|---|
| 把插件迁移到运行时入口 | 把混合的插件入口机械地迁移为显式的客户端入口和服务端入口。 | 迁移 |
把插件迁移到运行时入口
本指南用于把为 Paseo 0.7 或更早版本编写的插件,更新为 0.8 引入的当前客户端/服务端入口格式。
把本页交给一个编程 Agent,并以插件目录作为它的工作目录。按顺序执行各个步骤。不要保留兼容入口。
1. 对现有代码分类
从旧的结构开始:
my-plugin/
paseo-plugin.json
package.json
tsconfig.json
index.ts
greeting.client.tsx
greeting.server.ts
greeting.shared.ts
完成后的结构是:
my-plugin/
paseo-plugin.json
package.json
tsconfig.json
index.client.tsx
index.server.ts
client/greeting.tsx
server/greeting.ts
shared/greeting.ts
只创建插件需要的入口,至少需要一个。组件和客户端回调需要客户端入口。RPC 处理函数和 Node API 需要服务端入口。
2. 重命名文件和目录
严格按以下规则操作:
- 用
index.client.tsx、index.server.ts或两者替换混合的根入口。 - 把每个
name.client.ts或name.client.tsx移动到client/name.ts或client/name.tsx。 - 把每个
name.server.ts或name.server.tsx移动到server/name.ts或server/name.tsx。 - 把每个
name.shared.ts或name.shared.tsx移动到shared/name.ts或shared/name.tsx。 - 在对应的运行时目录下保留嵌套的功能目录。
- 每次移动后更新相对导入路径。
- 把
paseo-plugin.json、package.json和tsconfig.json保留在根目录。 - 删除旧的根入口。Paseo 不会加载它。
这些目录就是编译器的边界。client/ 下的文件只编译进应用的包,server/ 下的文件只编译进 daemon 的包,shared/ 则编译进两者。*.client.tsx 之类的文件名后缀不再有任何含义,留在插件根目录的代码模块会导致编译错误。
3. 迁移每一处注册
把下表作为完整的注册检查清单。
| 旧的注册及位置 | 新的注册及位置 |
|---|---|
旧根入口中的 plugin.handle(contract, handler) |
index.server.ts 中的 server.handle(contract, handler) |
旧根入口中的 plugin.addSurface(id, Component) |
index.client.tsx 中的 client.addScreen({ id, title, Component }) |
旧根入口中的 plugin.addSidebarItem({ id, title, icon, surface }) |
index.client.tsx 中的 client.addSidebarHeaderItem({ id, title, Component });见侧边栏项 |
| 新的侧边栏底部贡献 | index.client.tsx 中的 client.addSidebarFooterItem({ id, title, Component }) |
| 新的文件面板贡献 | index.client.tsx 中的 client.addFilePanel(panel) |
旧根入口中的 plugin.addWorkspacePanel(panel) |
index.client.tsx 中的 client.addWorkspacePanel(panel) |
旧根入口中的 plugin.addCommandCenterItem(item) |
index.client.tsx 中的 client.addCommandCenterItem(item) |
旧根入口中的 plugin.addClientSlashCommand(command) |
index.client.tsx 中的 client.addSlashCommand(command) |
旧根入口中的 plugin.addClientSide(fn) |
删除这层包装,把 fn 的函数体移入默认的客户端入口函数 |
旧客户端回调中的 client.addComposerPill(pill) |
index.client.tsx 或被导入的 client/ 函数中的 client.addComposerPill(pill) |
| 新的标题栏贡献 | client.addHeaderButton({ id, workspaceId, button }) |
旧根入口中的 plugin.addAttachmentSource(source) |
index.client.tsx 中的 client.addAttachmentSource(source) |
| 新的设置界面贡献 | index.client.tsx 中的 client.addSettingsScreen(screen);见设置界面 |
旧根入口中的 plugin.addTheme(theme) |
index.client.tsx 中的 client.addTheme(theme) |
旧根入口中的 plugin.addTimelineTransformer(transformer) |
index.client.tsx 中的 client.addTimelineTransformer(transformer) |
旧根入口中的 plugin.addTimelineRenderer(renderer) |
index.client.tsx 中的 client.addTimelineRenderer(renderer) |
shared 文件中的 import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin/server" |
import { defineRpc, defineAttachmentSource } from "@getpaseo/plugin" |
处理函数参数类型 ZodOutput<typeof contract.input> |
来自 @getpaseo/plugin 的 RpcInput<typeof contract>;返回类型使用 RpcOutput |
用原 addSidebarItem 项的 id 注册替代它的界面,这样已保存的链接仍然有效。/plugin/<plugin>/sidebar/<id> 链接以及更早的 /plugin/<plugin>/<id> 链接,会打开具有该 id 的 addSidebarItem,否则打开具有该 id 的界面。
从 @getpaseo/plugin/client 导入 PluginClientContext,从 @getpaseo/plugin/server 导入 PluginServerContext。删除对旧 context 类型的导入。客户端注册返回幂等的移除函数,但标题栏按钮和 Composer 标签除外,它们返回 { update, remove } 句柄。保留插件在销毁之前调用的所有移除函数;入口的清理函数运行后,Paseo 会移除尚未移除的注册。
Composer 标签
更新插件项目的 @getpaseo/plugin 依赖,然后运行 npm run typecheck。旧的贡献缺少必需的 button 字段,PluginComposerPillProps 不再导出,并且把新的注册当作函数调用会产生 TypeScript 错误。固定在旧 SDK 上的项目仍然按旧的契约检查;安装或重新加载插件并不会运行 TypeScript 的类型检查器。
把标签的 Component 替换为 button.icon 和 button.label,把 title 移入 button,把 onPress 移入 button.behavior。清理方式从调用返回的函数改为调用它的 .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 });
// Client entry cleanup:
pill.remove();
把原组件中的动态文本改为由你的模型或 SDK 订阅发出更新。自定义图标组件仍然可以使用 hook。Composer 标签总是显示图标和文字,并且从不显示展开箭头。菜单、弹出框和可见性见按钮。
4. 拆分导入
把 hook(usePaseo、useRpc、useSettings、useAgent、useWorkspace)和客户端贡献类型从 @getpaseo/plugin 移到 @getpaseo/plugin/client。把 Icon 移到 @getpaseo/plugin/client/ui。从 @getpaseo/plugin/server 导入服务端 context 和生命周期契约。共享的辅助函数(defineRpc、defineSettings、defineAttachmentSource)、schema 和普通数据类型保留在根入口。这些规则同样适用于类型导入。完整的契约见运行时模块。
把其余的 SDK 子路径移到它们所属的运行时下:
| 旧入口 | 当前入口 |
|---|---|
@getpaseo/plugin/react-native |
@getpaseo/plugin/client/ui |
@getpaseo/plugin/client/react-native |
@getpaseo/plugin/client/ui |
@getpaseo/plugin/ui |
@getpaseo/plugin/client/ui |
@getpaseo/plugin/provider |
@getpaseo/plugin/server/provider |
@getpaseo/plugin/acp |
@getpaseo/plugin/server/acp |
0.8 之前的入口和旧的 @paseo/plugin scope 已被移除。/client/host 是 Paseo 应用集成的私有模块,插件作者从不导入它。
客户端入口只导入 client/、shared/ 和可在客户端安全使用的包。服务端入口只导入 server/、shared/ 和可在服务端安全使用的包。在客户端入口或从它可达的任何代码中导入 node: 模块都会导致编译错误。不要仅仅为了接线注册而把组件导入服务端入口;那项注册属于客户端入口。
v0.13 中的 UI 导入
所有 Paseo UI 组件和 UI hook 都从 /client/ui 导入。之前的入口作为未写入文档的兼容别名保留到 2027-04-20。
对于逐步显示的时间线文本,把 useRevealedText(text, phase) 及其外层的 Text 换成 /client/ui 中的 <StreamedText text={text} phase={phase} style={style} />。插件设置的标题使用 SettingsSection;SettingsGroup 已弃用。
5. 识别只迁移了一半的错误
| 编译器或加载错误 | 含义与修复方法 |
|---|---|
This plugin has no requirements.paseo |
完成迁移,并按第 7 步声明版本范围。 |
This plugin was made for an older version of Paseo |
目录中仍然只有旧的根入口。创建一个运行时入口,迁移注册,然后删除旧文件。 |
Plugin entry points are missing: expected index.client.ts or index.client.tsx and/or index.server.ts or index.server.tsx |
不存在受支持的入口。至少添加一个文件名完全匹配的入口。 |
server-only module cannot be imported into the plugin client bundle: <file> |
某个客户端导入触及了 server/。把这个调用移到 RPC 之后,并从 shared/ 导入它的契约。 |
client-only module cannot be imported into the plugin server bundle: <file> |
某个服务端导入触及了 client/。把那项注册及其导入移到客户端入口。 |
Plugin modules belong in client/, server/, or shared/: <file> |
某个代码模块仍在插件根目录。把它移到对应的目录并修复它的导入。 |
Node module cannot be imported into the plugin client bundle: node:<name> imported by <file> |
客户端代码导入了 Node API。把该操作移到 server/,在 shared/ 中暴露一个 RPC,并从客户端调用它。 |
TypeScript 报告 PluginContext、addClientSide 或 addClientSlashCommand 不存在 |
按照上面的表格替换旧的 context 类型和注册。 |
6. 完整示例:plugin-examples/local-plugin
只有入口文件和导入路径发生变化。组件和处理函数的函数体原样移动,无需编辑。
迁移前:
local-plugin/
index.ts
main.client.tsx
increment.server.ts
increment.shared.ts
// index.ts
import type { PluginContext } from "@getpaseo/plugin";
import { contributeClient, ExamplePanel } from "./main.client";
import { increment } from "./increment.server";
import { incrementRpc } from "./increment.shared";
export default function contribute(plugin: PluginContext) {
plugin.handle(incrementRpc, increment);
plugin.addWorkspacePanel({
id: "counter",
title: "Plugin counter",
icon: "Blocks",
context: "workspace",
locations: ["workspace", "explorer"],
Component: ExamplePanel,
});
plugin.addCommandCenterItem({
id: "open-counter",
title: "Open plugin counter",
icon: "Blocks",
context: "workspace",
onSelect({ openPanel }) {
openPanel("counter");
},
});
plugin.addClientSide(contributeClient);
return () => {};
}
迁移后:
local-plugin/
index.client.tsx
index.server.ts
client/main.tsx # was main.client.tsx
server/increment.ts # was increment.server.ts
shared/increment.ts # was increment.shared.ts
// index.client.tsx
import type { PluginClientContext } from "@getpaseo/plugin/client";
import { contributeClient, ExamplePanel } from "./client/main";
export default function contribute(client: PluginClientContext) {
client.addWorkspacePanel({
id: "counter",
title: "Plugin counter",
icon: "Blocks",
context: "workspace",
locations: ["workspace", "explorer"],
Component: ExamplePanel,
});
client.addCommandCenterItem({
id: "open-counter",
title: "Open plugin counter",
icon: "Blocks",
context: "workspace",
onSelect({ openPanel }) {
openPanel("counter");
},
});
return contributeClient(client);
}
// index.server.ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { increment } from "./server/increment";
import { incrementRpc } from "./shared/increment";
export default function contribute(server: PluginServerContext) {
server.handle(incrementRpc, increment);
return () => {};
}
被移动文件中的导入路径变化:
// client/main.tsx
-import { incrementRpc } from "./increment.shared";
+import { incrementRpc } from "../shared/increment";
// server/increment.ts
-import { incrementRpc } from "./increment.shared";
+import { incrementRpc } from "../shared/increment";
contributeClient 原本就接收 PluginClientContext 并返回清理函数,因此客户端入口直接调用它并返回它的清理函数。如果插件的 addClientSide 回调还注册了标签或订阅,请保留那些代码;只去掉外层包装。
7. 声明 Paseo 版本要求
迁移完入口和导入后,在 paseo-plugin.json 中添加最低运行时版本:
{
"id": "my-plugin",
"requirements": { "paseo": ">=0.8.0" }
}
保留你现有的 ID 和构建命令。缺少 requirements.paseo 表示 <0.8.0,因此即使文件已经移动,Paseo 0.8 及更高版本也会拒绝该插件。仅仅添加这个字段并不会迁移代码。请把本地的 @getpaseo/plugin 开发依赖更新到你的目标版本,并在类型检查之前重新安装依赖。
版本范围和预发布版本的语义见版本要求。
8. 验证迁移
运行:
npm run typecheck
paseo plugin reload <plugin-id>
paseo plugin ls
确认状态为 running 且没有错误。试用每一项贡献。对于带 RPC 的插件,调用客户端操作并验证服务端结果。对于只有客户端的插件,确认贡献在没有服务端进程的情况下也能加载。对任何保存下来的注册移除函数调用两次,验证第二次调用不产生任何效果。