@plumeai/plugin-sdk 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,9 +25,10 @@ export default {
25
25
  | `ui.name` | 插件名 |
26
26
  | `ui.openSession(id)` | 用户点击后打开已有会话,和点侧栏那一行一样,不自动发送;没有的会话抛错 |
27
27
  | `ui.codeBlock(language, Component, { order, copyText })` | 按语言接管助手正文的围栏块,失败或停用显示源码,详见下文 |
28
- | `ui.toolCard(tool, Component, { order, match, inline })` | 给工具注册工具卡片;`match: "prefix"` 按工具名前缀匹配动态工具族;`inline: true` 时调用照常并进一段活 |
29
- | `ui.approvalCard(tool, Component, { order, match })` | 给自己的工具画审批卡;`match: "kind"` 按 `request.kind` 认 |
30
- | `ui.toolLine(tool, { verb, subject, like, group, times, callOnly, icon })` | 转录里怎么说自己的工具:行上的动作和对象、归哪一类、怎么数、图标;画法还是通用卡片 |
28
+ | `ui.toolCard(tool, Component, { order, match, inline })` | 给工具注册工具卡片;`match: "prefix"` 按工具名前缀匹配动态工具族;`inline: true` 时调用照常并进一段活,传函数 `(args, argumentsText) => boolean` 时逐次判断 |
29
+ | `ui.approvalCard(tool, Component, { order })` | 给自己的工具画审批卡,按 `request.tool` 认 |
30
+ | `ui.requestCard(kind, Component, { order })` | 给 `run.request` 画等人给数据的卡(问题、计划、表单),按 `request.kind` 认 |
31
+ | `ui.toolLine(tool, { verb, subject, file, note, command, job, group, apart, times, callOnly, icon }, { match? })` | 转录里怎么说自己的工具:行上的动作和对象、对着哪个文件、归哪一类、怎么数、图标;做的是哪种活看工具定义上的 `kind`,是不是记清单看定义上的 `category: "checklist"`;画法还是通用卡片;`match: "prefix"` 按前缀登记 |
31
32
  | `ui.panel(id, Component, options)` | 往面板区加一个面板 |
32
33
  | `ui.page(id, Component, options)` | 往侧栏加一个页面 |
33
34
  | `ui.skin({ name?, mode?, light, dark })` / `ui.skin(null)` | 更新皮肤或撤下自己的皮肤;name 与 mode 都提供时管理配色和深浅模式 |
@@ -39,7 +40,7 @@ export default {
39
40
  | `ui.composerChip(id, Component, { side, order })` | 输入框上方、项目签那一行加一格 |
40
41
  | `ui.composerAbove(id, Component, { order })` | 主输入框上方独占一行,收到 `{ session }`;返回 null 不占位,组件自己管理展开退场,不会浮到转录侧面 |
41
42
  | `ui.conversationCard(id, Component, { order, lane })` | 跟着会话的一张卡片:对话列够宽时浮在转录右上角,不够时在输入框正上方;`lane: true` 的占右边一列(够宽停靠、不够浮在转录上,从不到输入框上方,太长自己滚) |
42
- | `ui.environmentSection(id, Component, { label, order, attention? })` | 往会话的环境卡片(`plume.diagnostics` 画)里加一节,收到 `{ session }`;返回 null 不占位、不画分隔线;`label` 是卡片「显示哪些内容」菜单里的名字;`attention(session)` 是 hook,有事等人决定时返回一句话,卡片收着时开关上挂圆点 |
43
+ | `ui.environmentSection(id, Component, { label, order, attention? })` | 往会话的环境卡片(内置的 `plume.environment` 画)里加一节,收到 `{ session }`;返回 null 不占位、不画分隔线;`label` 是卡片「显示哪些内容」菜单里的名字;`attention(session)` 是 hook,有事等人决定、或有会自己叫醒这个会话的事时返回一句话,卡片收着时开关上挂圆点 |
43
44
  | `ui.mentions(provider)` | 每次打开 `@` 时传 `{ cwd?, sessionId? }`,返回 `ComposerMention[]` 或 Promise:`id/title/kind/token` 必填,kind 是 subagent 或 plugin,可带 description/origin/avatar;只插入 token,用户自行发送,停用自动撤销 |
44
45
  | `ui.composerHint(session => text)` | 输入框空着时的灰字建议,按 Tab 填进去 |
45
46
  | `ui.welcomeLine(locale => ({ text, source }))` | 换掉欢迎页问候下面的那一行(默认问要在哪个项目里做什么),只问最后注册的那一个 |
@@ -78,12 +79,15 @@ export default {
78
79
  | `ui.openPage(id)` | 人点了按钮时打开本插件注册的页面(输入框上方提示里的「查看」);不能开别的插件的页面 |
79
80
  | `ui.pageBadge(id, { count, label } \| null)` | 侧栏去处里本插件那一页的那一行右端挂一个数(没人看见的失败),`label` 给读屏和悬停提示;`null` 或 0 拿掉,人正在那一页上时不画,停用时一起拿掉 |
80
81
  | `ui.openExternal(url)` | 用户点击后在系统浏览器打开 HTTP(S) 链接,不接受带凭据的网址或其他协议 |
82
+ | `ui.openPath(path)` | 用户点击后把本机的文件交给默认程序、文件夹交给文件管理器打开;直接打开会运行它的(程序、脚本、应用)和网络共享上的改成在所在文件夹里指出来并提示;打不开时抛错(话给人看)。路径由逻辑入口算好经 `ctx.serve` 交回,逻辑入口不自己起系统的打开命令 |
83
+ | `ui.revealPath(path)` | 用户点击后在系统的文件管理器里打开上级目录并选中它(「在访达中显示」);和 `ui.openPath` 不同,文件夹不进到里面、文件不交给程序,所以不会运行它;不存在时抛错(话给人看)。路径同样由逻辑入口交回 |
81
84
  | `ui.openLink(url, sessionId)` | 用户点击后像回复里的网址那样打开:有插件接手点击(`takesClicks`,内置浏览器)时开在它那里,否则系统浏览器;限制同 `ui.openExternal` |
82
85
  | `ui.settingsSection(id, Component, options)` | 加一节设置:默认画进本插件详情的「设置」一节;`page: "nav"` 时单独占设置导航一行(`group` 选「Agent 能力」「数据」「代码与版本控制」组),给别的 `page` 时画进产品的那一页(「个性化」「智能体」,或「智能体」里「会话」那张卡的一行,或「插件市场」的「技能」「连接器」两类),见下文 |
83
86
  | `ui.ModelSelect` | 产品的模型下拉:`{ value, onChange(modelId), ariaLabel, inheritLabel, inheritDetail?, inheritedModelId?, inheritedSource?, disabled?, compact? }`;`value` 为空串是「跟随」,菜单顶上那一项就是它,选了 `onChange("")`;`compact` 只剩一枚箭头,贴在它管的按钮旁边当分体按钮(计划卡的「在新会话里执行」),选了什么由那颗按钮自己说 |
84
87
  | `ui.ModelControls` | 输入框右下角那两枚(模型、思考档位),样子和菜单都和输入框里的一样:`{ model, thinking?, onModelChange(modelId), onThinkingChange(level), disabled? }`;`model` 为空串、`thinking` 不给时显示默认模型和默认推理强度。不改哪个会话,只把选择交出来,给开新会话前先选好怎么跑的地方用(建议新会话的卡片),选的再交给 `ctx.sessions.launch` |
85
88
  | `ui.useModelDefaults()` | React hook:`{ defaultModel?, sessionModel?, thinking }`——新会话的默认模型、当前会话的模型、默认思考档位,「跟随」时写哪个模型名用它 |
86
89
  | `ui.useProjects()` | React hook:侧栏里的项目 `{ id, name, path }[]`,随增删更新;几个项目可能登记在同一个目录上 |
90
+ | `ui.usePluginDisplayName()` | React hook:返回 `(plugin) => string \| undefined`,按插件名(`owner`、`fromPlugin` 里的那种)查人看到的显示名,和侧栏会话卡片、「来自…」同一份;停用、卸载了或还没读到时是 undefined,退回一个通用的词,不把 `plume.xxx` 这种插件名画给人 |
87
91
  | `ui.screen` | 截屏的宿主能力:开截图浮层、全局快捷键、选文件夹 |
88
92
  | `ui.screenOverlay(Component)` | 截图浮层里画的组件 |
89
93
  | `ui.pinnedShot(Component)` | 钉在桌面上的截图窗口里画的组件 |
@@ -91,11 +95,11 @@ export default {
91
95
  | `ui.ComposerSend` | 产品的发送/停止按钮,收到 `{ running, disabled?, tip?, onSend, onStop }`;签名与使用见下文。经 `ui` 使用,不从 `@plume/ui` 导入 |
92
96
  | `ui.useSession(sessionId)` | React hook:一个会话的视图(摘要、消息、草稿、进度、审批),随它的事件更新;还没读到是 undefined |
93
97
  | `ui.useSessionActivity(sessionId)` | React hook:侧栏行首给这个会话的记号:`"waiting"`(它或子会话在等批准)、`"done"`、`"failed"`(人没看着时跑完、没跑完,人打开看过就清掉),没有记号是 undefined;只在内存里,重开应用都算看过。卡片上要「看过就不再提」时用它 |
94
- | `ui.contextBreakdown(sessionId)` | 这个会话的上下文窗口按段拆开:对话消息、工具、MCP 工具、技能目录、系统提示词、项目指令、项目记忆各多少 token(按最近一次请求估,有服务商的总数时按比例缩放到它),`memoryFiles` 是项目指令和项目记忆按文件的来源(`{ path, tokens }[]`,插件经段的 `files` 标的);本次启动后还没发过请求是 null。每次现算,要看时再调 |
95
98
  | `ui.listCommands({ cwd? })` | 输入框 `/` 菜单此刻有的命令(`ListedCommand[]`:`SlashCommand` 加 `plugin`),同名去重后的样子:产品自己的 `/clear`(没有 `plugin`)在前,再是各插件经 `ctx.commands` 供的;`cwd` 决定项目里的命令文件、技能列不列。只读,展开、发送仍走输入框;每次现问,要看时再调。命令设置页靠它列「内置」那一段(没有 `source` 的) |
96
99
  | `ui.Conversation` | 产品的对话视图组件:画一个会话(插件自己的子会话、独立会话),可选底下一个输入框 |
97
100
  | `ui.Markdown` | `{ text, sessionId?, streaming?, className?, inline? }`,复用产品正文渲染;指定 sessionId 时文件签按该会话解析,不指定时继承所在转录。`inline` 只画一句话里的行内记号(代码、加粗、强调、删除线),包在一个带 `className` 的 `span` 里,链接和图片只留字;代码和加粗的样子由宿主定(卡片底色、正文色、等宽代码字号),不跟着正文的代码外观设置走 |
98
101
  | `ui.remote<T>()` | 调自己逻辑入口经 `ctx.serve` 开的对象 |
102
+ | `ui.provide(name, service)` / `ui.useService(name)` | 界面入口之间的服务:一方提供,别的插件的组件里按名字拿到(没人提供是 undefined),见下文「服务」 |
99
103
 
100
104
  ## 会话变化与停止按钮
101
105
 
@@ -144,24 +148,37 @@ ui.codeBlock("my-language", ({ code, complete, send, fallback }) => {
144
148
 
145
149
  ## 工具在转录里的说法
146
150
 
147
- 不画卡片的工具,转录里用宿主的通用卡片:一行图标加一句做了什么,连着做的同类调用合成一段活。没登记说法时那句话就是工具名、自成一类、图标是终端那枚;登记了就和内置工具一个样子:
151
+ 不画卡片的工具,转录里用宿主的通用卡片:一行图标加一句做了什么,连着做的同类调用合成一段活。工具定义上没写 `kind`、这里也没登记说法时,那句话就是工具名、自成一类、图标是终端那枚。宿主不认任何工具名,读文件、跑命令这些基本工具(`plume.tools`)也是在定义上写 `kind`、在这里登记说法的:
148
152
 
149
153
  ```tsx
154
+ // 逻辑入口:做的是哪种活写在工具定义上,只写这一处(见 logic.md「按类别找工具」)
155
+ ctx.tools.register({ name: "symbol", kind: "search", /* … */ });
156
+
157
+ // 界面入口:怎么说
150
158
  ui.toolLine("symbol", {
151
159
  verb: (locale) => MESSAGES[locale].toolVerb, // 「查找符号」
152
160
  subject: (args) => (typeof args.name === "string" ? args.name : undefined),
153
- like: "read",
154
161
  });
155
162
  ```
156
163
 
157
164
  - 一次调用说成「动作 对象」(「查找符号 runAgent」),一段活里几次合在一起数个数(「查找符号 3 个」)。`verb` 写成事后描述的动词短语,和内置的「读取文件」「搜索内容」一个口吻。
158
165
  - `subject` 收到的参数是模型给的、没校验过:取不出就返回 undefined,只说动作;抛错也按没有对象处理。
159
- - `like` 是做的事像哪个内置工具(`read`、`grep`、`edit`、`write`、`bash`):读和搜索归「探索」、改和写归「改文件」、`bash` 归「跑命令」,连着做时并进同一段活;运行行用同一种说法;没给 `icon` 时用它的图标。不给就自成一类。
160
- - `group` 是插件自己起名的一类活(浏览器的五个工具都写 `"browser"`):同一类的连着做合成一段,和读、跑命令、改文件那几类也接成一段——边改边在页面上验是一件事。给了就不按 `like` 归类,`like` 照旧管运行行和图标。只该给「一段活里的一步」,要人单独看见的事(问人、派活)别给。
166
+ - 做的是哪种活不在这里写,看工具定义上的 `kind`(`ToolDefinition.kind`,宿主把工具表带给界面):`view` 读文件、`list` 列目录、`find` 按名字找文件、`search` 按内容搜、`create` 写出一个文件、`modify` 改已有的文件、`command` 跑一条命令、`watch` 起一条长跑的命令边跑边送输出、`job` 查看或停掉一个后台任务。前四种归「探索」、`create`、`modify` 归「改文件」、`command`、`watch`、`job` 归「跑命令」,连着做时并进同一段活;宿主按它给默认的说法(没给 `verb` 时「读取文件」「执行命令」……)、图标和运行行。一段活里有跑命令的(`command`、`watch`)时,查后台任务输出的那几次不另数。没有 `kind` 就自成一类。
167
+ - 下面几个从参数里取值,参数同样没校验过,取不出就返回 undefined:
168
+ - `file(args)`:这次调用对着的本机文件 `{ path, lines? }`(`path` 照模型写的样子,绝对路径或相对工作目录;`lines: { from, to? }` 是只读了其中几行;列目录的是目录,空串是工作目录)。没给 `verb` 时,`view`、`list` 的那一行说相对工作区的路径(`src/a.ts:10-29`、`src/`),别的说文件名;不是列目录、调用成功了的,回复里写到这个文件时直接画成文件签。
169
+ - `note(args)`:模型在参数里写给人看的一句说明(`description`),有就整行说它。命令、正则一眼读不出在干什么的工具给它。
170
+ - `command(args)`:`kind` 是 `command`、`watch` 的工具跑的命令原文。只是在读的命令(`rg`、`sed -n`、`git log`)归「探索」;参数还在生成、命令还没写出来时这次调用先不画。
171
+ - `job(args)`:`kind: "job"` 的工具这次查看或停掉的后台任务 `{ id?, stop? }`,`id` 对得上起它那次结果的 `details.job.id`(`BackgroundJobDetails`)。查看的那一行说是哪条命令的输出,连着查同一个任务的几次合成一行;`stop` 的那一行说停止命令。认不出是哪个任务时返回 undefined,这次调用仍按查看后台任务说,只是说不出是哪个任务。
172
+ - `create` 的参数要生成好一阵(整个文件的正文):还在生成时那一行先带上 `file` 取到的路径,行尾说写了几行。
173
+ - `group` 是插件自己起名的一类活(浏览器的五个工具都写 `"browser"`):同一类的连着做合成一段,和读、跑命令、改文件那几类也接成一段——边改边在页面上验是一件事。给了就不按工具定义上的 `kind` 归类,`kind` 照旧管说法、运行行和图标;不碰本机文件和命令、没有 `kind` 的工具(`plume.compaction` 的 `recall` 搜会话转录)也可以只给 `group` 和 `icon`。只该给「一段活里的一步」,要人单独看见的事(问人、派活)别给。
174
+ - `apart: true`(要和 `group` 一起给):这一类自成一段,只和同一类的接,不和读、跑命令、改文件接。例子是 `plume.mcp`:不管哪台服务的工具都是一类,和本地的活分开。
175
+ - 记进度清单的工具不在这里写,在工具定义上写 `category: "checklist"`(见 logic.md「按类别找工具」),结果的 `details` 带 `checklist`(`@plume/contract` 的 `ChecklistDetails`)。宿主从工具表里认出它,当一段活的记账:跟旁边那段活接成一段、不单独占一行,有别的活时总的那句不数它,回答之后被替人追问、只更新了清单的那一步不画,运行行说在排步骤。例子是 `plume.todo` 的 `todo_write`。
176
+ - `verb` 可以不给:按 `kind` 的说法说;也没有 `kind` 时只登记归类和图标,说法照没登记的工具(一次说工具名,合在一起说「使用工具」)。
177
+ - 工具名不固定时按前缀登记:`ui.toolLine("mcp__", { group: "mcp", apart: true }, { match: "prefix" })`。精确工具名登记的优先,几个前缀都对得上时取最长的。
161
178
  - `times: true`:几次合在一起时数次数(「操作网页 4 次」),不数个数。
162
179
  - `callOnly: true`:`verb` 只说这一次调用(跑着时那一行、展开后那一行),一段活收起后照没登记说法的工具说「使用工具」。给不算一件活、只是收尾手续的工具(子智能体交结果的 `yield`)。
163
180
  - `icon` 是 `@plume/ui/icons` 里的图标,13px。
164
- - 同时登记了 `toolCard` 的工具单独一行画卡片(`inline: true` 的照常并进一段活),`fallback` 用这里的说法。同一工具后登记的生效,它撤下后前一个恢复;插件登记的优先于产品给内置工具的说法。
181
+ - 同时登记了 `toolCard` 的工具单独一行画卡片(`inline: true` 的照常并进一段活),`fallback` 用这里的说法。同一工具后登记的生效,它撤下后前一个恢复。宿主不认插件的工具名:种类和是不是记清单看工具定义上的 `kind`、`category`,说法、图标和 `group` 归类看这里登记的。
165
182
 
166
183
  ## 工具卡片
167
184
 
@@ -179,7 +196,7 @@ ui.toolCard("my_tool", MyCard);
179
196
  - 工具卡片收到 `call`(`{ id, name, args }`)、`status`(`"approval" | "running" | "done" | "failed"`)、`progress`(执行中的输出)、`result`(执行完才有,含 `details`)、`fallback`(通用卡片)、`sessionId`(这张卡所在的会话);卡片在子会话里(侧边聊天)时还有 `handToParent(instruction)`:人点了按钮时把一件事排进那个子会话的父会话的队列,和 `run.parent.enqueue` 一样。
180
197
  - **先校验再画**:`details` 可能来自旧版本存下的会话,认不出就返回 `fallback`,不画半成品。
181
198
  - **动态工具名**:例如 MCP 可用 `ui.toolCard("mcp__", Card, { match: "prefix" })`。精确工具名的卡片优先于前缀卡片;所有匹配前缀按 order、注册先后排序。不匹配结果仍返回 fallback,停用后撤销前缀注册。
182
- - **只补东西的卡片**:登记了卡片的调用默认单独一行,不和前后的调用合成一段活。卡片只是在通用卡片下面补一点(认不出时直接返回 `fallback`)、调用本身该和同类的合在一起时,加 `inline: true`:调用照常按归类并进一段活,单独一次时那一行、展开一段活后的那一行由这张卡画。例子是 `plume.mcp` 的文档预览。
199
+ - **只补东西的卡片**:登记了卡片的调用默认单独一行,不和前后的调用合成一段活。卡片只是在通用卡片下面补一点(认不出时直接返回 `fallback`)、调用本身该和同类的合在一起时,加 `inline: true`:调用照常按归类并进一段活,单独一次时那一行、展开一段活后的那一行由这张卡画。例子是 `plume.mcp` 的文档预览。只有一部分调用画大东西、其余只画 `fallback` 时,`inline` 传函数逐次判断,返回 true 的那次并进一段活:参数还在写时 `args` 是空对象,只有 `argumentsText` 在长;只看这次调用自己的参数,参数写完后答案不能再变(一次调用从头到尾是转录里的同一行),不能等结果或后面的调用再定。例子是网页预览:截图检查只画一行,并进前后的活,发布的那次单独一行画网页。
183
200
  - **崩溃让位**:每张卡片外面有错误边界,渲染出错时让给同一工具的下一个注册者,都不行用通用卡片。组件里不用自己 try/catch。
184
201
  - **后注册的生效**:同一工具 `order` 小的在前;`order` 相同时后注册的在前,它撤下后前一个恢复。卡片的 `order` 默认是 0,写更小的 `order` 也能排到前面。
185
202
  - 外框用 `@plume/ui` 的 `Card`(`title`、`subtitle` 放命令或路径、`status`);输出用 `CodeBlock`,改动用 `ui.FileDiff`。
@@ -189,21 +206,37 @@ ui.toolCard("my_tool", MyCard);
189
206
  ## 审批卡
190
207
 
191
208
  ```tsx
192
- function PlanApproval({ request, sessionId, respond, permission, allowAuto, compact, fallback }: ApprovalCardProps) {
209
+ function ShellApproval({ request, sessionId, respond, permission, allowAuto, compact, fallback }: ApprovalCardProps) {
193
210
  if (!request.detail) return <>{fallback}</>;
194
- return <div className="my-card">…<Button onClick={() => void respond("once")}>接受</Button></div>;
211
+ return <div className="my-card">…<Button onClick={() => void respond("once")}>允许</Button></div>;
195
212
  }
196
- ui.approvalCard("submit_plan", PlanApproval);
213
+ ui.approvalCard("my_shell", ShellApproval);
197
214
  ```
198
215
 
199
- - 工具名不固定、一类问题从几个工具里问出来时(MCP 服务中途要人填的表单),按 `kind` 登记:`ui.approvalCard("mcp-form", FormCard, { match: "kind" })`,`run.ask` 时给同一个 `kind`。卡上要的结构化内容(表单的字段)写进 `detail`(JSON),认不出就返回 `fallback`。两种都对得上时按工具名登记的在前。
200
216
  - 只给自己的工具画(`request.tool`):卡片压在输入框正上方的那一格、排队一次一张、停下这一轮时撤卡归产品;组件画整张卡(外框、正文、按钮)。认不出这次请求就返回 `fallback`(产品的通用卡),渲染出错时让给下一个注册者,都不行用通用卡。
201
- - 回答只有 `"once"`、`"always"`、`"reject"`。卡上还有别的话要交给工具(修改意见、评论),先经 `ui.remote()` 交给自己的逻辑入口,再 `respond`;工具从 `run.ask` 返回以后去取。
217
+ - 回答只有 `"once"`、`"always"`、`"reject"`,问的是放不放行。要人给答案(回答问题、批准计划、填表)用下面的「等人给数据的卡」。
202
218
  - `permission` 是现在的权限模式;`allowAuto()` 只把请求批准切到帮我批准(「接受并开启自动模式」),先切再 `respond`,别的档调用不改。只在人点了卡上的按钮时调。
203
219
  - `compact`:窄窗口,面板区一次只摆一格,开面板会盖住卡片,要读的东西画在卡里。`from` 是问的子会话的标题,卡上要写明。
204
- - 没人答时卡会过期(权限请求 5 分钟,`userOnly` 的 30 分钟),截止时刻在 `request.expiresAt`;`queued` 是排在这张后面还在等的张数。外框想和产品的审批卡一样(收放的标题栏、倒计时、+N),用 `@plume/ui` 的 `ApprovalFrame`,正文和按钮放在里面;自己画外框的,标题行放一个 `ApprovalExpiry`(`at={request.expiresAt}`),有排队时写 `+{queued}`。例子是 `packages/builtin/ask-user`(`ApprovalFrame`)和 `plan` 插件(`ApprovalExpiry`)。
205
- - 卡片一出来把焦点给它,键盘能直接答;完全访问下要真弹给人,`run.ask` 带 `userOnly`(见 logic.md)。例子是 `plan` 插件。
206
- - 等这张卡时对话末尾、侧栏悬停和系统通知说的那句状态,`run.ask` 带 `waiting` 给(「等你批准计划」,按 `ctx.locale()`);不给时宿主按 `userOnly` 说「等待你的回答」或「等待你的批准」。
220
+ - 没人答时卡会过期(5 分钟),截止时刻在 `request.expiresAt`;`queued` 是排在这张后面还在等的张数。外框想和产品的审批卡一样(收放的标题栏、倒计时、+N),用 `@plume/ui` 的 `ApprovalFrame`,正文和按钮放在里面;自己画外框的,标题行放一个 `ApprovalExpiry`(`at={request.expiresAt}`),有排队时写 `+{queued}`。
221
+ - 等审批卡时对话末尾、侧栏悬停和系统通知说的是「等待你的批准」,插件改不了;要说自己的那句状态(「等你批准计划」),那就不是审批,用 `run.request` 的 `waiting`。
222
+
223
+ ## 等人给数据的卡
224
+
225
+ ```tsx
226
+ function QuestionCard({ request, answer, decline, from, queued, fallback }: RequestCardProps) {
227
+ const question = readQuestion(request.payload);
228
+ if (!question) return <>{fallback}</>;
229
+ return <ApprovalFrame …>…<Button onClick={() => void answer({ answer: picked })}>确认</Button><Button onClick={() => void decline()}>取消</Button></ApprovalFrame>;
230
+ }
231
+ ui.requestCard("ask_user", QuestionCard);
232
+ ```
233
+
234
+ - 工具经 `run.request({ kind, title, payload })` 要人给数据(见 logic.md),卡片按 `kind` 登记;`request.payload` 是工具给的数据,认不出就返回 `fallback`(产品的通用卡,只能「不答」)。渲染出错时让给下一个注册者。
235
+ - 人答了调 `answer(value)`,`value` 原样交回 `run.request`(工具那边会再核一遍,卡上也先查一遍,拦住手误);人不答调 `decline()`,工具得到 `reason: "declined"`。没送到时两者都抛错,卡片把错误摆出来、让人再答。一个回答连同附言(评论、修改意见)一起放进 `value`,不用经 `remote` 另交。
236
+ - 代管这个会话的插件可能在外面先答了(`ctx.sessions.answer`、`respond`,见 logic.md「代管会话」):卡和过期、停下时一样被产品收起,你的卡不用为它另做处理;那次调用底下由产品写「由 X 代答」。
237
+ - 摆在哪、排队、过期(30 分钟)、停下这一轮时撤卡和审批卡一样归产品;`from`、`queued`、`compact`、`permission`、`allowAuto()` 的意思同审批卡。外框用 `ApprovalFrame`,或自己画、标题行放 `ApprovalExpiry`。例子是 `packages/builtin/ask-user`(`ApprovalFrame`)、`plan` 插件(`ApprovalExpiry`、「接受并开启自动模式」先 `allowAuto()` 再 `answer`)和 `mcp` 插件的表单。
238
+ - 卡片一出来把焦点给它,键盘能直接答。
239
+ - 等这张卡时那句状态由 `run.request` 带 `waiting` 给(「等你批准计划」,按 `ctx.locale()`);不给时宿主说「等待你的回答」。
207
240
 
208
241
  ## 面板
209
242
 
@@ -275,12 +308,12 @@ ui.page("pull-requests", PullRequestsPage, { title, icon: GitPullRequest, topRow
275
308
 
276
309
  ```tsx
277
310
  ui.composerItem("capture", CaptureButton, { order: 0 }); // 左边,排在「添加附件」和权限后面
278
- ui.composerItem("context", ContextMeter, { side: "end" }); // 右边,排在模型选择前面
311
+ ui.composerItem("budget", BudgetMeter, { side: "end" }); // 右边,排在宿主的上下文用量和模型选择前面
279
312
  ```
280
313
 
281
- 新对话和会话里的输入框都有这一格。`side` 默认 `start`:左边放动作(截图);`end` 放跟下一轮的模型有关的读数(上下文用量)。组件收到 `sessionId`(新对话里还没有会话,是 undefined)、`running`(这个会话在跑一轮)、`model`(下一轮会用的模型:`id`、`name`、`contextWindow`;一个可用的都没有时为空),自己画按钮:`IconButton`,`size="composer"`,加 `emphasis`;有开、关两态的模式开关用 `ComposerToggle`(关着是淡色图标,开着带字、用强调色);读数这类不铺悬停底色的,自己的按钮加上 `plume-composer-control plume-composer-icon` 跟着工具栏的控件边长。会话的消息和用量用 `ui.useSession(sessionId)` 拿(回复的 `usage`),上下文按段的大小在点开时用 `ui.contextBreakdown(sessionId)` 要。这一格不该显示时(比如设置里关掉了)返回 null。
314
+ 新对话和会话里的输入框都有这一格。`side` 默认 `start`:左边放动作(截图);`end` 放跟下一轮的模型有关的读数(挨着宿主自己的上下文用量圆环)。组件收到 `sessionId`(新对话里还没有会话,是 undefined)、`running`(这个会话在跑一轮)、`model`(下一轮会用的模型:`id`、`name`、`contextWindow`;一个可用的都没有时为空),自己画按钮:`IconButton`,`size="composer"`,加 `emphasis`;有开、关两态的模式开关用 `ComposerToggle`(关着是淡色图标,开着带字、用强调色);读数这类不铺悬停底色的,自己的按钮加上 `plume-composer-control plume-composer-icon` 跟着工具栏的控件边长。会话的消息和用量用 `ui.useSession(sessionId)` 拿(回复的 `usage`)。这一格不该显示时(比如设置里关掉了)返回 null。
282
315
 
283
- 这一排放不下时,模型名先缩;缩到认不出,宿主按顺序藏掉标了 `data-plume-fit-drop` 的元素:`"1"` 先藏(权限签和 `ComposerToggle` 开着时的字,`ComposerToggle` 自己带着),`"2"` 再藏(上下文用量这类看一眼的读数)。量是宿主做的,插件只在可让的元素上标这个属性;不标的一直画着。
316
+ 这一排放不下时,模型名先缩;缩到认不出,宿主按顺序藏掉标了 `data-plume-fit-drop` 的元素:`"1"` 先藏(权限签和 `ComposerToggle` 开着时的字,`ComposerToggle` 自己带着),`"2"` 再藏(宿主的上下文用量,和插件加的这类看一眼的读数)。量是宿主做的,插件只在可让的元素上标这个属性;不标的一直画着。
284
317
 
285
318
  ## 项目签那一行
286
319
 
@@ -419,14 +452,14 @@ function SidePanel({ tab }: PanelProps) {
419
452
  ```
420
453
 
421
454
  - 不给 `composer` 就只读。默认按发送调 `send`,会话跑着时发送键换成停止键、按了调 `stop`,保持侧边聊天行为。`mode: "steer"` 时运行中也能发送,发送键不变停止键;`disabled` 保留但禁用操控区。停止可放在面板头部。
422
- - 输入框右下和主输入框一样:插件经 `ui.composerItem(…, { side: "end" })` 加的格子(上下文用量)会话在就画,拿到的是子会话;`model: "choose"` 再画能换的模型和思考档位(菜单最前面是「跟随主会话」,选了就清掉子会话自己的),`model: "show"` 画只读的模型和档位、悬停说明末尾接 `modelTip`。量的都是下一轮实际会用的:子会话没选的跟着父会话。会话还没建(第一句没发)时这些都不画;要让人在第一句之前就选,给 `blank: { parentId, model }`(第一句开在哪个会话下面、人没动过时插件要用的模型),`model: "choose"` 的模型和档位照画(没有用量格子),选的经 `send(text, choice)`、`sendMessage(message, choice)` 的 `choice`(`SessionChoice`:`model` 没有是跟随父会话)交回,建会话时照它给 `ctx.sessions.create({ parentId, ...choice })`。`footer` 放插件自己的东西,排在它们前面。
455
+ - 输入框右下和主输入框一样:插件经 `ui.composerItem(…, { side: "end" })` 加的格子和宿主的上下文用量会话在就画,量的是子会话;`model: "choose"` 再画能换的模型和思考档位(菜单最前面是「跟随主会话」,选了就清掉子会话自己的),`model: "show"` 画只读的模型和档位、悬停说明末尾接 `modelTip`。量的都是下一轮实际会用的:子会话没选的跟着父会话。会话还没建(第一句没发)时这些都不画;要让人在第一句之前就选,给 `blank: { parentId, model }`(第一句开在哪个会话下面、人没动过时插件要用的模型),`model: "choose"` 的模型和档位照画(没有用量格子),选的经 `send(text, choice)`、`sendMessage(message, choice)` 的 `choice`(`SessionChoice`:`model` 没有是跟随父会话)交回,建会话时照它给 `ctx.sessions.create({ parentId, ...choice })`。`footer` 放插件自己的东西,排在它们前面。
423
456
  - 提供 `sendMessage` 时输入框接入附件,交回完整 `OutgoingMessage`;不提供仍是纯文字。草稿与附件按子会话隔离,异步发送失败不会把文字还到另一个子会话。
424
457
  - 需要保留草稿、接收划词引用或弹出窗口时设置 `composer.draftKey: tab`(`PanelProps.tab`)。未发送文字、附件和引用按标签保存,支持 `sendMessage`;弹出、收回先确认存储成功,失败阻止交接并保留内存草稿。发送时引用附在消息文字前面,发送失败保留草稿,关闭标签后清理草稿;是否允许关闭仍由面板的 `onClose` 决定。
425
458
  - `renderUserMessage(message, fallback)` 只改当前视图用户消息的样子,可按 `origin === "parent"` 画任务卡,其余返回 fallback。`replyText(text, message)` 只过滤当前视图正文与复制,回报卡去重时只拿掉该条最终正文,不动工具和思考,也不改存储。回报卡复用 `ui.turnFooter`。
426
459
  - `reask: true`:人说的最后一句底下多一颗「编辑并重新提问」,宿主截掉它和之后的、拿改过的字重跑一轮(照常经 `prompt.submit`,插件按截掉后的记录算),不问要不要退文件;一轮在跑时置灰。没有撤回、分叉。侧边聊天开它,子 Agent 的全过程不开(话是派它的会话写的)。
427
- - 这个子会话经 `run.parent.enqueue`、卡片的 `handToParent` 交给父会话的事,宿主在转录和输入框之间画一条回执(排队中、在跑、做完、失败、随父会话停下,悬停有立即执行、撤回、重试、移除),插件什么都不用做;撤回的那句话放回 `composer.draftKey` 那份草稿,没给 `draftKey` 时不摆「撤回」。
460
+ - 这个子会话经 `run.parent.enqueue`、卡片的 `handToParent` 交给父会话的事走到了哪(`ChildTask`:排队中、在跑、做完、失败、随父会话停下)由宿主给,要画就给 `tasks={Component}`:组件收到 `{ sessionId, parentId, tasks, runNow, withdraw?, retry, dismiss }`,画在转录和输入框之间,留几件、摆哪些按钮由你定(侧边聊天的回执条见 `packages/builtin/side-chat/src/TaskStrip.tsx`);不给就不画。`withdraw` 把那件从父会话的队里收回、那句话放回 `composer.draftKey` 那份草稿,没给 `draftKey` 时没有它。操作出错时宿主弹提示条。
428
461
  - 没有「继续」:子会话的一轮只有插件能发起。等人批准的那一次压在主输入框上方,转录里只标「等待确认」。
429
- - 画的是独立会话(`create` 不给 `parentId`,见 logic.md「独立会话」)时:等人批准的那一次就压在这个视图的输入框上方(插件的 `ui.approvalCard` 照样用),因为它不挂在哪个对话下、没有别处可压;所以只在人看得到的地方画它,页面没开着时用 `ui.useSession` 读 `approval`、经 `ui.pageBadge` 带 `attention: true` 提醒人。`model: "choose"` 的菜单没有「跟随主会话」,没选的用默认模型;没有划词引用。
462
+ - 画的是独立会话(`create` 不给 `parentId`,见 logic.md「独立会话」)时:等人批准的那一次(和 `run.request` 等人给数据的卡)就压在这个视图的输入框上方(插件的 `ui.approvalCard`、`ui.requestCard` 照样用),因为它不挂在哪个对话下、没有别处可压;所以只在人看得到的地方画它,页面没开着时用 `ui.useSession` 读 `approval`、经 `ui.pageBadge` 带 `attention: true` 提醒人。`model: "choose"` 的菜单没有「跟随主会话」,没选的用默认模型;没有划词引用。
430
463
  - 外面那一层给它高度(面板里 `height: 100%`),它自己撑满、底下贴着输入框。
431
464
 
432
465
  ## 设置分区
@@ -512,7 +545,7 @@ function ModelSetting() {
512
545
 
513
546
  `ui.skin({ light: { … }, dark: { … } })`,只能覆盖 `SkinToken` 列出的 token(就是 `--plume-*` 去掉前缀),值是 CSS 值。颜色由四个种子 `bg`、`fg`、`accent`、`contrast`(0–1)推出整套面、线、字,通常只给种子;推出来的哪一档不满意再单独覆盖。同一时间只有一个皮肤,后注册的生效,它撤下后前一个恢复;同一入口可反复调用更新,`ui.skin(null)` 只撤下自己的皮肤。
514
547
 
515
- 同时提供 `name`(非空、最多 80 字符)和 `mode: "system" | "light" | "dark"` 时成为管理配色的数据皮肤。宿主暂时忽略用户保存的界面种子与深浅模式,外观页实时显示当前皮肤名和只读配色;字体、代码外观和其他偏好仍可改,原配色不被改写,皮肤停用后恢复。只提供 `{ light, dark }` 的旧皮肤不接管偏好,用户明确设置的项仍优先。深浅模式由宿主主进程按窗口暂存,插件不访问内部配置或 `window.plume`。
548
+ 同时提供 `name`(非空、最多 80 字符)和 `mode: "system" | "light" | "dark"` 时成为管理配色的数据皮肤。宿主暂时忽略用户保存的界面种子与深浅模式,外观页实时显示当前皮肤名和只读配色;字体、代码外观和其他偏好仍可改,原配色不被改写,皮肤停用后恢复。只提供 `{ light, dark }` 的普通皮肤不接管偏好,用户明确设置的项仍优先。深浅模式由宿主主进程按窗口暂存,插件不访问内部配置或 `window.plume`。
516
549
 
517
550
  只传 `{ name: "Plume", mode: "system" }`、不传 token 时,使用产品原生配色并跟随系统;`light`、`dark` 则固定模式。这和 `ui.skin(null)` 不同:前者暂时管理配色并屏蔽原有种子覆盖,后者退出管理、恢复原用户偏好。
518
551
 
@@ -540,20 +573,49 @@ apply(ui) {
540
573
  - 每个方法返回一个调用:`await` 拿结果;逻辑入口返回异步迭代器的,`for await` 逐个拿中间值;`.cancel()` 取消,对端的 `signal` 中止。组件卸载时记得取消还在流的调用。
541
574
  - 逻辑入口没开或插件停用了,调用以错误结束,界面要能显示这种状态。
542
575
 
576
+ ## 服务:给别的插件的界面入口用
577
+
578
+ 一个插件的界面入口提供实现,别的插件的界面入口按名字拿来用,两边都不用知道对方是谁。和逻辑入口的服务(`ctx.provide` / `ctx.use`)是同一个意思,但不经宿主排加载先后:
579
+
580
+ ```tsx
581
+ // 提供的一方
582
+ apply(ui) {
583
+ ui.provide("background.jobs.view", { open: (sessionId, jobId) => { /* 选中任务,ui.openPanel(...) */ } });
584
+ }
585
+
586
+ // 用的一方:在组件里拿
587
+ function OpenJob({ sessionId, jobId }: { sessionId: string; jobId: string }) {
588
+ const viewer = ui.useService("background.jobs.view");
589
+ if (!viewer) return null; // 没人提供:不给这个入口
590
+ return <Button onClick={() => viewer.open(sessionId, jobId)}>查看输出</Button>;
591
+ }
592
+ ```
593
+
594
+ - 服务的接口是 `@plume/contract/ui` 里的 `UiServices`。自己定义的服务用声明合并加一项:`declare module "@plume/contract/ui" { interface UiServices { "my.thing": { open(id: string): void } } }`。服务名说能力,不带插件名。
595
+ - `ui.useService` 是 React hook(规则同 hook),只在组件里调。没人提供(提供方没装、停用了)时是 `undefined`,用的一方别画那个入口或别让它能点;不用写 `inject`,也不会因此等着不加载。
596
+ - 同一项服务有几个插件提供时后提供的生效(界面入口先装内置的、再装本地上传和市场的,所以装一个提供同名服务的插件就换掉内置的),它停用后前一个恢复;同一插件再提供同名的换掉自己前一个。提供方换了、撤了,用它的组件跟着重画。
597
+ - 拿到的就是提供方给的那个对象,宿主不包一层;别往上面挂东西。
598
+ - 本体自己也要用的(打开网址、终端、文件预览)不是服务,是按种类登记的接手者(`ui.linkOpener`、`ui.terminalOpener`、`ui.documentViewer`)。
599
+
600
+ Plume 自带的界面侧服务:
601
+
602
+ | 服务 | 提供方 | 接口 |
603
+ | --- | --- | --- |
604
+ | `background.jobs.view` | 内置插件 plume.diagnostics(选中那个任务、打开任务面板) | `open(sessionId, jobId)`:人点了对话里某个后台任务(逻辑入口里 `ctx.sessions.jobs` 列出的那些)那一行时调,不改变送达或命令状态。plume.tools 画的「后台命令完成」那一行用它,没人提供时那一行不给点 |
605
+
543
606
  ## 共享模块
544
607
 
545
- `react`、`react/jsx-runtime`、`@plume/ui`、`@plume/styles` 不打包,运行时用产品的那一份;其余(包括 `@plume/ui/icons` 和第三方库)打进 `dist/ui.mjs`。能用哪些组件、各自的签名和用法,见 [components.md](components.md)(从源码生成);`@plume/ui` 里没列在那里的导出是产品内部用的,引用了 `pnpm check:plugins` 会拦。
608
+ `react`、`react/jsx-runtime`、`@plume/ui`、`@plume/styles` 不打包,运行时用产品的那一份;其余(包括 `@plume/ui/icons` 和第三方库)打进 `dist/ui.mjs`。宿主启动时就加载每个插件的 `ui.mjs`:只在某个操作里才用到的大库(预览、图表、编辑器)写成 `await import("…")`,构建时拆到 `dist/chunks/`,用到时才取。能用哪些组件、各自的签名和用法,见 [components.md](components.md)(从源码生成);`@plume/ui` 里没列在那里的导出是产品内部用的,引用了 `pnpm check:plugins` 会拦。
546
609
 
547
- ## 运行诊断入口
610
+ ## 运行诊断与环境卡片入口
548
611
 
549
612
  - `ui.sessionToolbar(id, Component)` 在有会话的标题栏放局部控件,收到 `{ session }`;新会话不画,不能接管标题与布局。使用 `IconButton`,停用自动撤下。
550
613
  - `ui.sessionMenuItem(id, { label, icon?, run })` 在会话操作菜单加一项。`run` 收到被点的会话摘要,异步操作固定使用它的 id,不跟随焦点;报错由宿主提示。同插件同 id 替换。
551
- - `ui.deliveryAction("job", (sessionId, id) => ...)` 接后台送达行的查看入口,只在人点击时调用。后注册的生效,撤下恢复前一个;不改变送达或命令状态。
552
- - `ui.agentDelivery(Component)` 画本插件的子会话交回的那一行(`ctx.sessions.report` 带了 `delivery` 的):收到 `{ sessionId, reports }`,一次送达里本插件交的几份一起给,画成一行(子 Agent 的「消息来自 @explore」,用 `FlowRow`)。宿主记着每份是哪个插件交的,只交给那个插件登记的组件;再登记换掉自己前一个,停用时撤下,没登记时不画,组件出错时只丢这一行。
614
+ - `ui.agentDelivery(Component)` 画本插件交回的结果在对话里的那一行(`ctx.sessions.report`、`channel.report`、`reportBack` 带了 `delivery` 的):收到 `{ sessionId, reports }`,一次送达里本插件交的几份一起给(连着几次只带 `inline` 的送达并成一次),画成一行(子 Agent 的「消息来自 @explore」、plume.tools 的「后台命令完成」,用 `FlowRow`)。每份的 `details` 是你交的时候给的自己的数据,宿主不读,原样带回来。宿主记着每份是哪个插件交的,只交给那个插件登记的组件;再登记换掉自己前一个,停用时撤下;没登记(或停用了)时宿主按 `title`、`status` 画默认的一行,带 `sessionId` 的(新会话交回)画成「来自新会话」、点了过去;组件出错时只丢这一行。
553
615
  - `ui.ProjectOpener` 以 `{ path }` 显示目录的打开方式,复用文件预览的默认应用与选择菜单;只有用户点击才调用系统打开。
554
- - `ui.environmentSection(id, Component, { label, order?, attention? })` 往环境卡片里加一节:和会话相关的一件周边的事,每一行只是入口,点了去自己的页面或面板(`ui.openPage`、`ui.openPanel`)。没东西时返回 null。order 排进卡片自己的几节之间:打开方式 0、后台任务 10、子 Agent 20、计划 30、定时唤醒 50、侧边聊天 70、压缩记录 85、引用 90。卡片不替插件画小标题,要小标题的自己画(文字用 `Text size="caption" tone="faint"`)。例子:`plume.pull-requests` 用 60 放当前分支的拉取请求(一行,没有小标题);`plume.summary` 用 80 放会话摘要(带「摘要」小标题)。`attention` 是一个 React hook(规则同 hook),这一节有事等人回来决定时返回一句话(按界面语言),没有返回 undefined:卡片收着、这一节没被藏起来时,标题栏的环境开关上挂一个圆点,这句话接在开关的说明后面。只为「等人决定」,不为「有东西在跑」。例子:内置的 `plume.spawn-session` 用 40 放「建议的新会话」,还没人点开始的建议经 `attention` 说「N 条建议的新会话等你决定」。
616
+ - `ui.environmentSection(id, Component, { label, order?, attention? })` 往环境卡片里加一节:和会话相关的一件周边的事,每一行只是入口,点了去自己的页面或面板(`ui.openPage`、`ui.openPanel`)。没东西时返回 null。order 排进卡片自己的几节(打开方式 0、子会话 20(按建它的插件分几节,节名是 `ui.usePluginDisplayName` 查到的显示名)、引用 90)和别的插件的节之间;内置插件已经用了的:后台任务 10、定时唤醒 50、压缩记录 85(`plume.diagnostics`),计划 30(`plume.plan`),建议的新会话 40(`plume.spawn-session`)。卡片不替插件画小标题,要小标题的自己画(文字用 `Text size="caption" tone="faint"`)。例子:`plume.pull-requests` 用 60 放当前分支的拉取请求(一行,没有小标题);`plume.summary` 用 80 放会话摘要(带「摘要」小标题)。`attention` 是一个 React hook(规则同 hook),这一节有要提醒人的事时返回一句话(按界面语言),没有返回 undefined:卡片收着、这一节没被藏起来时(只在这时调,要读的数据可以只在被调时拉),标题栏的环境开关上挂一个圆点,这句话按节的 order 接在开关的说明后面。要提醒的是两种事:等人回来决定的,和会自己叫醒这个会话的(人不回来,会话也会自己开一轮);普通的「有东西在跑」不算(开着一个开发服务器是常态),算进去圆点就一直亮着。例子:内置的 `plume.spawn-session` 用 40 放「建议的新会话」,还没人点开始的建议经 `attention` 说「N 条建议的新会话等你决定」;`plume.diagnostics` 的后台任务一节说「N 个监视在跑」(监视有输出就开一轮,普通后台命令不算),定时唤醒一节说「N 个定时唤醒待触发」。
555
617
  - `ui.useEnvironmentSections()` 是给画环境卡片的插件用的 React hook:返回各插件登记的节(`key` 为 `插件名/id`、`label`、`order`、已包好错误边界的 `component`),按 order 排好。
556
618
 
557
- 内置 `plume.diagnostics` 用以上入口和 `ui.panel`、`ui.conversationCard` 提供任务、轨迹与环境卡。工具结果和历史正文读 `SessionView`,观察数据与后台任务经自己的 `remote` 读取。
619
+ 内置 `plume.diagnostics` 用以上入口和 `ui.panel` 提供任务和轨迹,经 `ui.environmentSection` 往环境卡片里放后台任务、定时唤醒、压缩记录三节;工具结果和历史正文读 `SessionView`,观察数据与后台任务经自己的 `remote` 读取。环境卡片本身是内置的 `plume.environment`:`ui.sessionToolbar` 放开关,`ui.conversationCard`(`lane: true`)画卡片,`ui.useEnvironmentSections()` 拿各插件的节,挂上各节的 `attention` 决定开关上的圆点;它不认是谁登记的节,两个插件互不点名。
558
620
 
559
621
  `ui.pageBadge` 的 badge 可带 `attention: true`,同时在通知菜单中显示该页面的待处理事项;label 是通知标题,点击打开该页面。