@vetta-org/plugin-sdk 0.3.0 → 0.3.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.
- package/docs/.source.json +22 -0
- package/docs/README.md +104 -0
- package/docs/ability-details.md +424 -0
- package/docs/ai.md +121 -0
- package/docs/app-actions.md +111 -0
- package/docs/browser.md +80 -0
- package/docs/conversation-and-agent.md +619 -0
- package/docs/file-explorer.md +118 -0
- package/docs/getting-started.md +295 -0
- package/docs/guiding-the-agent.md +183 -0
- package/docs/manifest.md +296 -0
- package/docs/mcp.md +152 -0
- package/docs/media.md +141 -0
- package/docs/message-cards.md +188 -0
- package/docs/permissions.md +145 -0
- package/docs/styling-and-pitfalls.md +254 -0
- package/docs/system-plugins.md +58 -0
- package/docs/ui-slots.md +628 -0
- package/package.json +6 -3
package/docs/ui-slots.md
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# UI 扩展点
|
|
2
|
+
|
|
3
|
+
`ctx.ui` 提供 UI 注册点(消息卡片另见 [message-cards.md](./message-cards.md))。每个注册都返回 `Disposable`(`{ dispose() }`);插件卸载时宿主会统一处置,无需手动调用。
|
|
4
|
+
|
|
5
|
+
> 所有 slot 组件经 Module Federation 与宿主共享 React 单例,可直接用 hook、直接传组件实例。注意[顶层 JSX 陷阱](./styling-and-pitfalls.md#module-federation-顶层-jsx-陷阱)。
|
|
6
|
+
|
|
7
|
+
会话页相关 slot(活动 Tab、输入栏 toggle、Turn 卡)支持 **`scope_use`**:**fail-closed**——未声明或空数组 = **任何对话场景都不显示**;声明后仅在列出的场景出现(如 `["project", "conversation"]`)。场景 slug 见 [conversation-and-agent.md](./conversation-and-agent.md#scope_use按对话场景限定工具出现范围)。
|
|
8
|
+
|
|
9
|
+
## 全局通知 notify
|
|
10
|
+
|
|
11
|
+
向宿主右下角全局 Toast 推送一条通知,**无需权限**。用于把失败原因暴露给用户(比只写红字 / `console.error` 更可操作)。
|
|
12
|
+
|
|
13
|
+
- 权限:无
|
|
14
|
+
- 传入 `error` 时:variant 默认 `error`、Toast **不自动消失**,并提供 **「复制堆栈」** 一键复制(含 `pluginId@version`、Error.stack / 序列化详情);同时 `console.error` 一份便于 DevTools
|
|
15
|
+
- React 组件拿不到 `ctx`:在 `activate` 里把 `ctx.ui.notify` 存到模块变量再调用
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface PluginNotifyOptions {
|
|
19
|
+
message: string; // 用户可见摘要(必填)
|
|
20
|
+
title?: string; // 默认插件展示名
|
|
21
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
22
|
+
error?: unknown; // 有则附加「复制堆栈」
|
|
23
|
+
durationMs?: number; // 0 = 手动关闭;有 error 时默认 0
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// 模块级捕获,供预览 / 面板组件使用
|
|
29
|
+
let notify: import("@vetta-org/plugin-sdk").PluginUiApi["notify"];
|
|
30
|
+
|
|
31
|
+
function PptxPreview({ file }: PluginFilePreviewProps) {
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let cancelled = false;
|
|
34
|
+
// 二进制预览优先 getUrl(见下文「大文件」),勿默认 readBytes
|
|
35
|
+
const load = async () => {
|
|
36
|
+
const url = file.getUrl();
|
|
37
|
+
const bytes = url
|
|
38
|
+
? await (await fetch(url)).arrayBuffer()
|
|
39
|
+
: await file.readBytes();
|
|
40
|
+
return parse(bytes);
|
|
41
|
+
};
|
|
42
|
+
load()
|
|
43
|
+
.then((_result) => {
|
|
44
|
+
if (cancelled) return;
|
|
45
|
+
// setSlides(_result) …
|
|
46
|
+
})
|
|
47
|
+
.catch((err) => {
|
|
48
|
+
if (cancelled) return;
|
|
49
|
+
notify({
|
|
50
|
+
message: "无法解析此 PPTX 文件",
|
|
51
|
+
error: err, // 用户可点「复制堆栈」
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
};
|
|
57
|
+
}, [file]);
|
|
58
|
+
// ...
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default definePlugin({
|
|
62
|
+
activate(ctx) {
|
|
63
|
+
notify = ctx.ui.notify;
|
|
64
|
+
ctx.ui.registerFilePreview({ extensions: ["pptx"], component: PptxPreview });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**何时必须 notify(推荐规范)**
|
|
70
|
+
|
|
71
|
+
| 场景 | 做法 |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| 读文件 / 解析 / 网络 / 外部库失败 | `notify({ message: 用户可读摘要, error })`,UI 仍可显示简短失败态 |
|
|
74
|
+
| 权限 / 配置缺失 | `warning` + 可引导 `openPluginSettings()`(若适用) |
|
|
75
|
+
| 纯成功反馈 | 可选 `variant: "success"`,短 `durationMs` |
|
|
76
|
+
| 可预期的空态(无数据) | **不要**当错误 notify,用组件内 empty UI |
|
|
77
|
+
|
|
78
|
+
禁止:只 `catch (() => setError("失败"))` 且不传 `error`——用户与 agent 都无法拿到堆栈。
|
|
79
|
+
|
|
80
|
+
## 全局浮层 registerGlobalSlot
|
|
81
|
+
|
|
82
|
+
在 App 根部渲染一个组件(全局浮层 / 对话框 / 常驻 UI)。
|
|
83
|
+
|
|
84
|
+
- 权限:`ui.slot.global`(缺权限 **warn+noop**)
|
|
85
|
+
- 组件:**零 props,自包含**。
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
ctx.ui.registerGlobalSlot({ id: "panel", component: MyPanel });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
interface PluginGlobalSlotContribution {
|
|
93
|
+
id: string; // 插件内唯一;宿主命名空间化为 `${pluginId}:${id}`
|
|
94
|
+
component: ComponentType; // 零 props
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
典型用途:设置缺失引导弹窗、全局悬浮工具。一个插件可注册多个。
|
|
99
|
+
|
|
100
|
+
## 工作区视图 registerWorkspaceView
|
|
101
|
+
|
|
102
|
+
贡献一个**整页 surface**,与内置的「自动化」「知识库」同级:宿主给它一条自己的路由
|
|
103
|
+
`/workspace/<pluginId>/<viewId>` 和一个侧边栏导航入口,打开后整个内容区都归插件。
|
|
104
|
+
|
|
105
|
+
- 权限:`ui.slot.workspace-view`(缺权限 **warn+noop**)
|
|
106
|
+
- 导航入口默认落在侧边栏的「更多」收纳里;用户可以拖动排序,也可以 **pin 到左上方置顶区**(含「新会话」最多 5 个),布局按 key 持久化
|
|
107
|
+
- 组件收到 `{ pluginId, viewId }`,一个组件可以服务多个注册
|
|
108
|
+
- `icon` 是 **iconify class 字符串**(如 `"icon-[solar--widget-4-linear]"`),不是 ReactNode——宿主要把它渲染进自己的导航按钮,并按 key 持久化布局
|
|
109
|
+
- **不写 `icon` 就用插件自己的 Logo**:宿主回落到 `plugin.json` 的 `icon`。包内图片(`svg` / `png` / `webp` 等)默认按主题前景色 mask 成**单色**,因此自带图形的插件不必去图标集里找一个近似的;Iconify 名照常当 class 用。两者都不存在时才落到宿主默认图标
|
|
110
|
+
- **`iconTint: false` 保留原图色彩**:导航项改用 `<img>` 渲染。选之前先掂量:入口只有 16px、与内置单色图标并排,且固定色彩无法跟随主题——深色 logo 会在深色侧边栏里消失。**只对彩色 logo 有意义**:单色图形 tint 后反而更清晰统一,而整块不透明的彩色图 tint 后会糊成一个纯色块。对 Iconify class 图标无效(它们始终跟随主题色)
|
|
111
|
+
- **`sidebar: false` 不占导航位**:视图只出现在「设置 → 更多选项」,宿主在设置壳内打开它(两层侧栏保留,切换其它插件页面是一次点击)。配置页、安装引导、诊断台这类「装完就不常回来」的 surface 应该选它——侧边栏是用户自己策划的稀缺空间,每个插件都常驻一格,会把用户真正高频的入口挤进收纳菜单
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
interface PluginWorkspaceViewContribution {
|
|
116
|
+
id: string; // 插件内唯一;进 URL,故限 [a-z0-9][a-z0-9._-]*
|
|
117
|
+
label: string; // 侧边栏文案,支持 %catalogKey%
|
|
118
|
+
icon?: string; // iconify class 字符串
|
|
119
|
+
iconTint?: boolean; // 图片图标是否按主题色染成单色,缺省 true
|
|
120
|
+
description?: string; // 导航项 tooltip
|
|
121
|
+
badge?: PluginNavBadge; // 导航项角标,见下
|
|
122
|
+
component: ComponentType<PluginWorkspaceViewProps>;
|
|
123
|
+
navOrder?: number; // 同一插件内多个视图的排序
|
|
124
|
+
sidebar?: boolean; // 是否占侧边栏导航位,缺省 true;false = 只在设置 → 更多选项
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
ctx.ui.registerWorkspaceView({
|
|
130
|
+
id: "board",
|
|
131
|
+
label: "%view.board.label%",
|
|
132
|
+
icon: "icon-[solar--widget-4-linear]",
|
|
133
|
+
badge: { kind: "beta" },
|
|
134
|
+
component: BoardView,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 程序化跳转到自己的视图
|
|
138
|
+
ctx.ui.openWorkspaceView("board");
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 导航项角标
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
type PluginNavBadge =
|
|
145
|
+
| { kind: "beta" } // 宿主预置,见下
|
|
146
|
+
| { kind: "text"; text: string; tone?: PluginNavBadgeTone }
|
|
147
|
+
| { kind: "count"; count: number; tone?: PluginNavBadgeTone }
|
|
148
|
+
| { kind: "dot"; tone?: PluginNavBadgeTone };
|
|
149
|
+
|
|
150
|
+
type PluginNavBadgeTone = "default" | "accent" | "warning" | "danger";
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `beta` 是**宿主预置**:渲染成与内置「知识库」一模一样的 Beta 标识,文案由宿主按当前语言给出,插件不必自己把 "Beta" 翻译一遍
|
|
154
|
+
- `text` 支持与 `label` 相同的 `%catalogKey%` 目录解析;解析后为空则不出角标
|
|
155
|
+
- `count` 超过 99 显示 `99+`,**归零时角标消失**(挂一个「0」比没有更糟)
|
|
156
|
+
- `tone` 只能从上述语义值里选,宿主映射到自己的主题色——插件给不了原始色值,角标因此始终与内置导航项一致
|
|
157
|
+
|
|
158
|
+
运行时更新用 `setWorkspaceViewBadge`,**不要靠重新注册**——那会让整个整页 surface 重挂载,未读数变一下就丢掉视图内部状态:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
ctx.ui.setWorkspaceViewBadge("board", { kind: "count", count: unread });
|
|
162
|
+
ctx.ui.setWorkspaceViewBadge("board", { kind: "dot", tone: "warning" });
|
|
163
|
+
ctx.ui.setWorkspaceViewBadge("board", null); // 清掉
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
认不出的角标(未知 `kind`、空文本、非有限数)一律当作「没有角标」,不会让视图注册失败。
|
|
167
|
+
|
|
168
|
+
### 接管宿主页头 setWorkspaceViewHeader
|
|
169
|
+
|
|
170
|
+
宿主页头(窗口顶部那条)在工作区视图路由上照常渲染,缺省显示应用名。插件如果再画一条
|
|
171
|
+
自己的顶栏,用户看到的就是两条叠在一起。`setWorkspaceViewHeader` 让视图把自己的标题与
|
|
172
|
+
工具栏**搬进**那条栏:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
ctx.ui.setWorkspaceViewHeader("board", {
|
|
177
|
+
hideTitle: true, // 收掉宿主标题,自己在 left 里写
|
|
178
|
+
left: <BoardTitle keyword={keyword} onKeywordChange={setKeyword} />,
|
|
179
|
+
right: <BoardActions busy={busy} onCreate={create} />,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
useEffect(() => () => ctx.ui.setWorkspaceViewHeader("board", null), []);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- 权限:`ui.slot.workspace-view`(与注册同一个);视图未注册时 **warn+noop**
|
|
186
|
+
- 只在**该视图自己的路由**上生效;用户切走后页头自动回到宿主标题,不需要手动撤销
|
|
187
|
+
- 这是**实时 setter**,不是一次性注册:`left` / `right` 是普通 ReactNode,闭包里的状态变了
|
|
188
|
+
就重新推一次(放在没有依赖数组的 effect 里最省心)。宿主在插件自己的 i18n 目录与 CSS
|
|
189
|
+
`@scope` 内渲染它们,`useTranslation()` 和插件的 Tailwind 类照常可用
|
|
190
|
+
- 节点必须带 `no-drag`:整条页头是窗口拖拽区,不摘出来的话输入框和按钮点不动
|
|
191
|
+
- 视图注销、插件停用或重载时,接管会被宿主自动撤下
|
|
192
|
+
- 页头本身**不会**被移除:它同时是窗口拖拽区与 macOS 红绿灯安全区,还挂着侧边栏展开
|
|
193
|
+
按钮。这个 API 是「占用它」,不是「换掉它」
|
|
194
|
+
- `immersive: true` 让页头**浮在视图之上**而不是把视图往下推:视图占满全高,顶端约
|
|
195
|
+
44px 处在透明页头条下方(拖拽、红绿灯、侧边栏展开按钮照常工作在最上层)。适合门面
|
|
196
|
+
从窗口第一像素开始的沉浸式整页;页头里放了 `left`/`right` 工具栏时不要开——工具栏
|
|
197
|
+
会压在内容上
|
|
198
|
+
|
|
199
|
+
**该用哪个插槽**
|
|
200
|
+
|
|
201
|
+
| 场景 | 用 |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| 跨会话、跨项目的工作台(看板、控制台、仪表盘) | **工作区视图** |
|
|
204
|
+
| 绑定当前对话的辅助面板 | [活动 Tab](#活动面板-tab-registeractivitytab) |
|
|
205
|
+
| 全局浮层 / 对话框 | [全局浮层](#全局浮层-registerglobalslot) |
|
|
206
|
+
|
|
207
|
+
**与面板类插槽的关键差别**:工作区视图**独占内容区**,所以它可以(也应该)自带
|
|
208
|
+
滚动容器,不受「面板内禁止 viewport 级浮层」的约束。但它仍在宿主窗口内——不要覆盖
|
|
209
|
+
宿主 chrome(侧边栏、标题栏)。页面级 header 优先用
|
|
210
|
+
[`setWorkspaceViewHeader`](#接管宿主页头-setworkspaceviewheader) 搬进宿主那条栏,
|
|
211
|
+
而不是在内容区里再画一条。
|
|
212
|
+
|
|
213
|
+
插件被禁用时,它的导航入口消失;用户如果正停在该路由上,宿主会把他送回首页。
|
|
214
|
+
持久化的侧边栏布局按 key 保留,插件装回来后位置复原。
|
|
215
|
+
|
|
216
|
+
示例:`packages/plugins/externals/kanban`。
|
|
217
|
+
|
|
218
|
+
## 文件预览 registerFilePreview
|
|
219
|
+
|
|
220
|
+
按**文件扩展名**贡献预览组件,渲染在活动面板的文件预览区。
|
|
221
|
+
|
|
222
|
+
- 权限:`ui.slot.file-preview`(缺权限 **warn+noop**)
|
|
223
|
+
- **优先级=仅补空白**:内置已支持的扩展名(image / audio / pdf / docx / markdown / json / 常见文本)插件**抢不到**;只有内置不认、本会掉进文本兜底的扩展名才查插件注册表,**首个匹配胜**。
|
|
224
|
+
- 组件收到 `file` prop —— 宿主**不**预读、不替你猜编码。
|
|
225
|
+
- **布局边界(面板内)**:预览组件必须把 UI 限制在预览壳内。禁止 `fixed` / 视口级定位、禁止超高 `z-index` 抢宿主 chrome、禁止 `createPortal` 到 `document.body`。面板内浮层用根节点 `relative` + 子节点 `absolute`。全局浮层走 [`registerGlobalSlot`](#全局浮层-registerglobalslot);错误/提示走 [`notify`](#全局通知-notify)。宿主会对 file-preview 壳做 fixed containing block + overflow 裁剪作为兜底——**仍须按规范写**。细则与正反例见 [styling-and-pitfalls.md → 面板类 slot 布局边界](./styling-and-pitfalls.md#面板类-slot-布局边界禁止-viewport-级浮层)。
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
interface PluginPreviewFile {
|
|
229
|
+
path: string | null;
|
|
230
|
+
name: string;
|
|
231
|
+
extension: string; // 小写、不含点
|
|
232
|
+
mime: string;
|
|
233
|
+
size: number; // 字节;未知时可能为 0——不要假定很小
|
|
234
|
+
readText(): Promise<string>;
|
|
235
|
+
readBytes(): Promise<ArrayBuffer>;
|
|
236
|
+
getUrl(options?: { mediaKind?: "audio" | "video" }): string; // Range 流式 URL
|
|
237
|
+
/** 磁盘变更时回调(防抖);无真实 path 时为 no-op。返回 Disposable。 */
|
|
238
|
+
watch(listener: () => void): Disposable;
|
|
239
|
+
/** 本地音频元数据(标题/封面等);不支持时返回 null。 */
|
|
240
|
+
getAudioMetadata?(): Promise<PluginAudioMetadata | null>;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
ctx.ui.registerFilePreview({ extensions: ["svg"], component: SvgPreview });
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
完整示例见 `packages/plugins/presets/svg-viewer`、`media-viewer`、`office-viewer`。
|
|
249
|
+
|
|
250
|
+
### 内容访问:三种 API 怎么选
|
|
251
|
+
|
|
252
|
+
| API | 适用 | 宿主行为 / 限制 |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| **`getUrl()`** | **二进制 / 可能偏大 / 媒体 / 可流式解析**(PDF、Office zip、音视频、大图) | 返回 `vetta-media://…`(或远程 url)。**支持 Range**;`fetch(url)` 或交给原生 `<audio>`/`pdf.js` 等。**无整文件 10MB 封顶**(相对 IPC 全量读)。 |
|
|
255
|
+
| **`readBytes()`** | 仅当库**必须**拿到完整 `ArrayBuffer` 且你已接受体积风险 | 经 IPC 全量读盘。**硬上限约 10MB**——更大直接抛错(如 `File too large to preview (>10 MB)`)。base64 往返,内存与序列化成本高。 |
|
|
256
|
+
| **`readText()`** | 明确的小文本(svg 源、json、轻量 xml) | 同样走 IPC;**大文本同样不适合**。 |
|
|
257
|
+
|
|
258
|
+
**Agent / 作者硬规则(文件预览插件不要敷衍)**
|
|
259
|
+
|
|
260
|
+
1. **默认按「用户可能打开几十 MB~上百 MB」设计**,不要只拿 100KB 样例验收。
|
|
261
|
+
2. **能流式就流式**:优先 `const url = file.getUrl()`,再 `fetch(url)` / 交给支持 URL 的引擎。官方 `office-viewer` 模式:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
async function fetchFileBytes(file: PluginPreviewFile): Promise<ArrayBuffer> {
|
|
265
|
+
const url = file.getUrl();
|
|
266
|
+
if (!url) return file.readBytes(); // 仅 url-only 兜底
|
|
267
|
+
const res = await fetch(url);
|
|
268
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
269
|
+
return res.arrayBuffer();
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
3. **禁止**无脑 `readBytes()` 当唯一路径,然后 catch 成一句「无法解析」——大文件会先撞 10MB 墙,用户只看到含糊失败。
|
|
274
|
+
4. 若格式**必须**整包进内存(如整 zip 解压):
|
|
275
|
+
- 仍优先 `getUrl` + `arrayBuffer()`(绕开 IPC 10MB);
|
|
276
|
+
- 读 `file.size`:过大时**提前**友好提示(可给阈值,如「超过 N MB 仅支持元数据/前几页」),不要等 OOM;
|
|
277
|
+
- 无法支持的巨大文件:组件内说明 + `notify({ message, error 或 说明 })`,**不要静默挂死**。
|
|
278
|
+
5. **加载态 / 取消**:`useEffect` 里 `cancelled` 标志;卸载后不 `setState`;长时间解析显示 loading,必要时分片/只解析需要的部分(如 pptx 只读 `ppt/slides/*`,不必把整包图片解码进 UI)。
|
|
279
|
+
6. **媒体类**(音/视频)直接用 `getUrl({ mediaKind })` 作 `src`,**禁止** base64 塞进内存。
|
|
280
|
+
7. 失败一律带原始错误:`notify({ message: "…", error })`(见 [notify](#全局通知-notify))。
|
|
281
|
+
|
|
282
|
+
### 反例与正例
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
// ❌ 敷衍:一律 readBytes,无 size 意识,吞错误
|
|
286
|
+
file.readBytes().then(parse).catch(() => setError("失败"));
|
|
287
|
+
|
|
288
|
+
// ✅ 优先流式 URL;大文件友好;错误可复制堆栈
|
|
289
|
+
const url = file.getUrl();
|
|
290
|
+
const bytes = url
|
|
291
|
+
? await (await fetch(url)).arrayBuffer()
|
|
292
|
+
: await file.readBytes();
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
若引擎支持 URL/Range(PDF.js 等),**连整包 arrayBuffer 都可省**:
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
// ✅ 最佳:引擎自己拉流
|
|
299
|
+
pdfjs.getDocument({ url: file.getUrl() });
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## 活动面板 Tab registerActivityTab
|
|
303
|
+
|
|
304
|
+
向活动面板注册一个 tab。
|
|
305
|
+
|
|
306
|
+
- 权限:`ui.slot.activity-tab`(注册 **warn+noop**;`openActivityTab` / `setActivityTabVisible` **抛错**)
|
|
307
|
+
- **`scope_use` fail-closed**(必写,否则任何场景不显示)
|
|
308
|
+
- **默认注册即上栏**(`initiallyVisible` 缺省 `true`)。声明 `initiallyVisible: false` 表示「出现条件我自己管」:注册只入池,之后用 `setActivityTabVisible` 静默上栏/下栏(如 git 只在仓库目录上栏、工作台跟随输入栏 toggle),或用 `openActivityTab` 上栏并抢焦点打开(如图像生成完成后跳到历史)
|
|
309
|
+
- 显隐记录按 **会话 cwd** 持久化(ADR-0026):插件表过态就听插件的,没表过态才看 `initiallyVisible`。用户随时可用减号手动隐藏
|
|
310
|
+
- 插件禁用时 tab 隐藏,重新启用可回来
|
|
311
|
+
- **默认按访问驻留**:首次激活后进入宿主的 warm LRU,切换时保留组件状态与 DOM;每个面板最多保留 2 个非活动 warm tab,超出的旧 tab 在浏览器空闲阶段淘汰。`retention: "active-only"` 可关闭驻留,`retention: "pinned"` 表示只要贡献可用就不参与淘汰。当前活动和浮动 tab 始终驻留。
|
|
312
|
+
- **布局边界(面板内)**:与 file-preview 相同——UI 留在 Tab 面板矩形内,禁止 viewport 级 `fixed` / 超高 z-index / portal 到 `document.body`。全局浮层用 `registerGlobalSlot`,Toast 用 `notify`。见 [styling-and-pitfalls.md → 面板类 slot 布局边界](./styling-and-pitfalls.md#面板类-slot-布局边界禁止-viewport-级浮层)。
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
interface PluginActivityTabContribution {
|
|
316
|
+
id: string;
|
|
317
|
+
label: string; // 可用 %catalogKey%(见 i18n)
|
|
318
|
+
icon?: ReactNode;
|
|
319
|
+
component: ComponentType; // 零 props
|
|
320
|
+
scope_use?: readonly ConversationScenario[]; // fail-closed
|
|
321
|
+
initiallyVisible?: boolean; // 缺省 true:注册即上栏;false = 出现条件由插件自己驱动
|
|
322
|
+
retention?: "active-only" | "warm" | "pinned"; // 缺省 warm
|
|
323
|
+
/** @deprecated true=pinned,false=active-only */
|
|
324
|
+
keepAliveWhenAvailable?: boolean;
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
ctx.ui.registerActivityTab({
|
|
330
|
+
id: "stats",
|
|
331
|
+
label: "%tab.label%",
|
|
332
|
+
icon: <StatsIcon />,
|
|
333
|
+
component: StatsPanel,
|
|
334
|
+
scope_use: ["project", "conversation"],
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
组件零 props。**面板作用域用 `useActivityTab()` 取 cwd**,不要用 `useActiveConversation().cwd` 代替(项目详情页面板 cwd 是项目的,活动会话可能属于别的项目):
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { useActivityTab } from "@vetta-org/plugin-sdk";
|
|
342
|
+
|
|
343
|
+
function StatsPanel() {
|
|
344
|
+
const { cwd, active } = useActivityTab();
|
|
345
|
+
// warm/pinned 会保留组件;昂贵的轮询或动画应在 active=false 时主动暂停。
|
|
346
|
+
// ...
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### openActivityTab
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
ctx.ui.openActivityTab(tabId, options?: { width?: number | "max"; cwd?: string });
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
编程方式 attach(如需)并激活本插件某个 tab;`width: "max"` 尽量拉满(宿主仍 clamp)。载荷经插件自己的内存状态传递。缺省作用于宿主当前显示的会话;tool handler 与后台任务应传入触发会话的 `cwd`,避免异步执行期间用户切换前台后串写到别的项目。
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
ctx.ui.openActivityTab("preview", { width: "max", cwd: session.cwd });
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`width` **只在该 tab 首次 attach 时生效**:tab 已 attach 的重复调用(含 reload/热更新导致的 `activate()` 重放)只做激活,不会覆盖用户手动拖出的面板宽度。
|
|
363
|
+
|
|
364
|
+
示例:`packages/plugins/presets/git`、`externals/mobile-ui-preview`。
|
|
365
|
+
|
|
366
|
+
### setActivityTabVisible
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
ctx.ui.setActivityTabVisible(tabId, visible: boolean, options?: { cwd?: string });
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
只把 tab 放进/移出目标会话的标签栏,**不激活、不展开面板**——「它现在该不该在栏里」,而不是「用户此刻要看它」。`cwd` 的缺省与显式目标语义同 `openActivityTab`。这是插件表达自己出现条件的地方:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// git:只在 git 工作区里上栏。conversation.on 订阅后会立刻回放一次
|
|
376
|
+
// conversation-changed,所以不用等下次切会话。
|
|
377
|
+
ctx.conversation.on((event) => {
|
|
378
|
+
if (event.type !== "conversation-changed") return;
|
|
379
|
+
const { cwd } = event.conversation;
|
|
380
|
+
if (!cwd) return;
|
|
381
|
+
void isInsideGitWorkTree(ctx.command, cwd).then((inRepo) =>
|
|
382
|
+
ctx.ui.setActivityTabVisible("changes", inRepo),
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// 插件工作台:跟随输入栏 toggle(硬隔离只负责关掉时藏起来,不会帮你上栏)。
|
|
387
|
+
ctx.ui.registerInputAction({
|
|
388
|
+
id: "mode",
|
|
389
|
+
hardIsolation: true,
|
|
390
|
+
onToggle: (active) => ctx.ui.setActivityTabVisible("workbench", active),
|
|
391
|
+
// ...
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
上栏记录按 cwd 持久化,所以**只需在条件变化时调用**;用户之后用减号手动隐藏的结果不会被重复调用覆盖。当前没有活动会话时是 no-op(无处记录),插件应在会话就绪后重新判定。异步判定要注意丢弃过期结果:写入落在**调用时**的活动会话上,探测期间切走了就别再写。
|
|
396
|
+
|
|
397
|
+
## 输入栏动作 registerInputAction
|
|
398
|
+
|
|
399
|
+
在 AI 输入栏下方加一个**开关型动作按钮**(toggle)。激活时,宿主在每次发送前调用 `decoratePrompt()`,把元数据和插件隐藏指令合并进外发 prompt。
|
|
400
|
+
|
|
401
|
+
- 权限:`ui.slot.input-action`(缺权限**抛错**)
|
|
402
|
+
- **`scope_use` fail-closed**;与 `requiresActiveTool` **取「与」**才显示
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
interface PluginInputActionContribution {
|
|
406
|
+
id: string;
|
|
407
|
+
label: string;
|
|
408
|
+
icon?: ReactNode;
|
|
409
|
+
defaultActive?: boolean;
|
|
410
|
+
requiresActiveTool?: string; // 仅当该 agent 工具在本会话激活时显示
|
|
411
|
+
scope_use?: readonly ConversationScenario[];
|
|
412
|
+
/** 见下文「插件贡献硬隔离」 */
|
|
413
|
+
hardIsolation?: boolean;
|
|
414
|
+
onToggle?(active: boolean): boolean | void; // 返回 false 可否决激活
|
|
415
|
+
decoratePrompt?(): {
|
|
416
|
+
metadata?: Record<string, unknown>;
|
|
417
|
+
instructions?: string[];
|
|
418
|
+
} | void;
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
ctx.ui.registerInputAction({
|
|
424
|
+
id: "image-mode",
|
|
425
|
+
label: "%action.imageMode.label%",
|
|
426
|
+
icon: <IconImage />,
|
|
427
|
+
scope_use: ["conversation", "project"],
|
|
428
|
+
requiresActiveTool: "generate_image",
|
|
429
|
+
onToggle: (active) => {
|
|
430
|
+
if (active && notConfigured()) {
|
|
431
|
+
showSettingsGuard();
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
decoratePrompt: () => ({
|
|
436
|
+
instructions: ["Produce an actual image this turn by calling the appropriate plugin tool."],
|
|
437
|
+
}),
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### 软隔离 vs 硬隔离(内置对照)
|
|
442
|
+
|
|
443
|
+
- **图像生成(软)**:工具不因 toggle 关闭而剥离;插件通过 `instructions` 加强本轮图像意图。
|
|
444
|
+
- **知识检索(硬,宿主内置)**:未开 toggle 时本轮剥离 `kb-read` 工具。
|
|
445
|
+
|
|
446
|
+
### 插件贡献硬隔离 hardIsolation
|
|
447
|
+
|
|
448
|
+
`hardIsolation: true` 时(ADR-0041):
|
|
449
|
+
|
|
450
|
+
- Toggle **默认关**时,该插件的 **tools / skills / MCP / systemPrompt 贡献**不进入 agent;**Activity Tab 也隐藏**。
|
|
451
|
+
- Toggle 打开后恢复贡献(宿主 `setContributionMode` + `reconfigureAgentPlugins`)。
|
|
452
|
+
- 可与清单 `contributionMode.hardIsolation` 联用(冷启动即 gate,见 [manifest](./manifest.md#contributionmode))。
|
|
453
|
+
- **用户自建插件默认不要开**;模式型系统插件(如插件工作台)使用。
|
|
454
|
+
|
|
455
|
+
`requiresActiveTool`:badge 跟随工具 `scope_use`,避免工具被场景屏蔽时仍显示无效开关。
|
|
456
|
+
|
|
457
|
+
配套:`setPromptAttachment`(通用一次性 prompt 上下文胶囊)、`previewImage`(全屏图片预览)——见 [conversation-and-agent 私有存储 API](./conversation-and-agent.md#插件私有存储-api)。
|
|
458
|
+
|
|
459
|
+
## 新会话上下文区 registerNewSessionContext
|
|
460
|
+
|
|
461
|
+
在**新会话页**输入框下方铺一块内容:用户接下来多半要用到的素材(风格库、模板墙、最近产物)。与 input-action 的分工——那是发送前的**开关**,这是发送前的**上下文**。
|
|
462
|
+
|
|
463
|
+
- 权限:`ui.slot.new-session-context`(缺权限 **warn+noop**)
|
|
464
|
+
- 上屏与否**由宿主裁决**,插件只负责被激活后渲染
|
|
465
|
+
- 同时有多个贡献上屏时,该区域顶部出 tabbar;只有一个时直接渲染内容
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
interface PluginNewSessionContextContribution {
|
|
469
|
+
id: string;
|
|
470
|
+
label: string; // tabbar 标题;只有一个贡献上屏时不展示
|
|
471
|
+
icon?: ReactNode; // 省略时用插件自己的图标
|
|
472
|
+
activateWhen: PluginNewSessionContextActivation;
|
|
473
|
+
width?: "input" | "wide"; // 默认 "input"
|
|
474
|
+
render(context: PluginNewSessionContext): ReactNode;
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
ctx.ui.registerNewSessionContext({
|
|
480
|
+
id: "design-styles",
|
|
481
|
+
label: "%tab.label%",
|
|
482
|
+
// 选中本插件的设计师、或在输入框提到本插件的 skill 时上屏。
|
|
483
|
+
activateWhen: { agents: ["designer"], skills: ["vetta-ui-design"] },
|
|
484
|
+
// 画廊类内容压在输入框宽度里,每一项都会小到看不出风格。
|
|
485
|
+
width: "wide",
|
|
486
|
+
render: (context) => <DesignStyleLibrary context={context} />,
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 激活条件 activateWhen
|
|
491
|
+
|
|
492
|
+
**只能写本插件自己的东西**,填别人的 id 一律不生效。这条限制是故意的:否则插件 A 可以声明「只要用户选了 B 的智能体我就上屏」,把别人的使用场景劫持过来。
|
|
493
|
+
|
|
494
|
+
| 字段 | 含义 |
|
|
495
|
+
| --- | --- |
|
|
496
|
+
| `agents` | 本插件在 manifest `agent.agents` 里声明的智能体 id(**不带 `plugin:` 前缀**)。省略表示「本插件的任意智能体」。 |
|
|
497
|
+
| `teams` | 把团队命中收窄到指定团队。团队本身按**成员**推导:队里有本插件贡献的角色就算相关,通常省略即可。 |
|
|
498
|
+
| `skills` | 本插件提供的 skill 名;用户在输入框里提到时命中。 |
|
|
499
|
+
| `mcpServers` | 本插件提供的 MCP server 名。 |
|
|
500
|
+
|
|
501
|
+
- 各字段取**并集**:任意一条命中即激活。
|
|
502
|
+
- **至少要声明一条**:全空等于「任何新会话都上屏」,那不是上下文区该有的行为,宿主直接抛错。
|
|
503
|
+
- 上屏顺序:选中目标(`target`)排在提及能力(`mention`)之前——选了设计师,设计资源就该是第一个 tab,而不是因为另一个插件装得早就抢到首位;同强度按插件 id 与注册顺序稳定排序。
|
|
504
|
+
|
|
505
|
+
> ⚠️ `teams` 当前按**宿主团队 id** 比对,而插件贡献的团队在宿主侧用的是推导 id,写 manifest 里的团队 id 匹配不上。要按团队激活,先靠成员推导(省略 `teams`)。
|
|
506
|
+
|
|
507
|
+
### 渲染上下文
|
|
508
|
+
|
|
509
|
+
`render` 拿到的对象是一个**会继续生长**的结构:按需解构,不要假定它只有这些键。
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
interface PluginNewSessionContext {
|
|
513
|
+
target: { kind: "agent" | "team"; id: string; contributedId?: string } | null;
|
|
514
|
+
mentionedAbilities: { skills: readonly string[]; mcpServers: readonly string[] };
|
|
515
|
+
draft: string; // 需 conversation.draft.read,未授予时恒为空串
|
|
516
|
+
cwd: string | null; // 未选择项目时 null
|
|
517
|
+
composer: {
|
|
518
|
+
attach(attachment: PluginPromptAttachment): void;
|
|
519
|
+
insertText(text: string, options?: { position?: "start" | "end" }): void;
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
- `target.contributedId`:目标由本插件贡献时,给出 manifest 里的那个 id。
|
|
525
|
+
- `draft` **只在本贡献处于激活状态时提供**——插件拿不到「用户随便打点什么」的全程流水。
|
|
526
|
+
- `composer.insertText` 始终是**插入**而非替换,免得抹掉用户已经打的内容;`position` 缺省 `"end"`(光标处),`"start"` 适合「先定题、细节由用户补」。
|
|
527
|
+
- 刻意**不提供「直接发送」**:越过发送前这道关不属于本区职责。
|
|
528
|
+
|
|
529
|
+
### 宽度与位置
|
|
530
|
+
|
|
531
|
+
- `width: "input"`(默认)与输入框同宽,适合补充说明一类的窄内容。
|
|
532
|
+
- `width: "wide"` 铺满页面可用宽度(窄窗口铺满、宽屏取八成),留给画廊、素材墙这类「内容本身就是主角」的东西。
|
|
533
|
+
- 该区在输入栏**下方**独立成块,跟着内容长高;命令面板展开时整块让位——那是打断式交互。
|
|
534
|
+
- 没有贡献上屏时连槽位都不给,页面不会留一块空白。
|
|
535
|
+
|
|
536
|
+
### 团队会话
|
|
537
|
+
|
|
538
|
+
团队会话与单智能体会话一样会把**工作区**与**轮次开始**报给插件,因此本区里挂的东西(附件、插入的文本)在团队会话里同样有去处。
|
|
539
|
+
|
|
540
|
+
## 工具行内渲染 registerToolCallSlot
|
|
541
|
+
|
|
542
|
+
按 **toolName** 替换宿主默认的工具调用行内 UI(transcript 内嵌渲染)。**首个注册胜**。
|
|
543
|
+
|
|
544
|
+
- 权限:`ui.slot.tool-call`(缺权限**抛错**)
|
|
545
|
+
- 与消息卡片互补:卡片挂在消息下方;本槽替换**工具调用那一行**的渲染
|
|
546
|
+
- 插件工具也可返回 `cards` 走消息卡片(见 [message-cards](./message-cards.md));需要行内富 UI 时用本 API
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
ctx.ui.registerToolCallSlot({
|
|
550
|
+
id: "my-tool-ui",
|
|
551
|
+
toolName: "my_tool",
|
|
552
|
+
component: MyToolCallView, // props: { toolCall: { toolCallId, toolName, args, status, result?, isError? } }
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## 本轮 Turn 卡 registerTurnCard
|
|
557
|
+
|
|
558
|
+
在消息列表底部(**最新一轮**)挂一张**不绑定 tool 调用**的卡片。宿主挂载组件;插件自行决定可见性(不适用时 `return null`)。
|
|
559
|
+
|
|
560
|
+
- 权限:`ui.slot.turn-card`(缺权限**抛错**)
|
|
561
|
+
- **`scope_use` fail-closed**
|
|
562
|
+
- 典型:git「本轮变更」卡(只在仓库有变更时显示)
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
ctx.ui.registerTurnCard({
|
|
566
|
+
id: "changes",
|
|
567
|
+
component: GitTurnCard,
|
|
568
|
+
scope_use: ["project"],
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
示例:`packages/plugins/presets/git`。
|
|
573
|
+
|
|
574
|
+
## 键盘快捷键 registerShortcutScope
|
|
575
|
+
|
|
576
|
+
把绑定挂到宿主统一的 **ShortcutScopeStack**(与宿主 UI 同一条链路:`modal` > `overlay` > `surface` > `app`)。**不要**在插件里 `document.addEventListener("keydown")`。
|
|
577
|
+
|
|
578
|
+
- 权限:`ui.shortcuts.register`(缺权限**抛错**)
|
|
579
|
+
- 可用 kind:`surface` | `overlay` | `modal`(**不能**用 `app`——留给宿主可配置的全局动作)
|
|
580
|
+
- 键格式与宿主 `eventToShortcut` 一致,如 `"mod+s"`、`"escape"`、`"arrowleft"`、`"="`、`"-"`
|
|
581
|
+
- `when`:`always`(默认)| `editable` | `not-editable`
|
|
582
|
+
- 组件内优先用 `usePluginShortcutScope`(在 `activate` 里把 `ctx.ui.registerShortcutScope` 存到模块变量,再传给 hook)
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
// activate
|
|
586
|
+
let registerShortcutScope = ctx.ui.registerShortcutScope.bind(ctx.ui);
|
|
587
|
+
// 或:setRegisterShortcutScope((c) => ctx.ui.registerShortcutScope(c));
|
|
588
|
+
|
|
589
|
+
// React 组件
|
|
590
|
+
import { usePluginShortcutScope, type PluginShortcutBinding } from "@vetta-org/plugin-sdk";
|
|
591
|
+
|
|
592
|
+
const bindings: PluginShortcutBinding[] = [
|
|
593
|
+
{ key: "=", when: "not-editable", run: () => zoomIn() },
|
|
594
|
+
{ key: "-", when: "not-editable", run: () => zoomOut() },
|
|
595
|
+
{ key: "0", when: "not-editable", run: () => resetZoom() },
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
usePluginShortcutScope(registerShortcutScope, {
|
|
599
|
+
id: "zoom",
|
|
600
|
+
kind: "surface",
|
|
601
|
+
bindings,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// 全屏时用更高 kind,避免被宿主 surface(如文件预览 Esc)抢走
|
|
605
|
+
usePluginShortcutScope(registerShortcutScope, {
|
|
606
|
+
id: "fullscreen-esc",
|
|
607
|
+
kind: "overlay",
|
|
608
|
+
active: isFullscreen,
|
|
609
|
+
bindings: [{ key: "escape", run: () => exitFullscreen() }],
|
|
610
|
+
});
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
命令式(`activate` 或副作用里):
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
const handle = ctx.ui.registerShortcutScope({
|
|
617
|
+
id: "panel-keys",
|
|
618
|
+
kind: "surface",
|
|
619
|
+
exclusive: false,
|
|
620
|
+
enabled: () => panelOpen,
|
|
621
|
+
bindings: () => [
|
|
622
|
+
{ key: "escape", run: () => closePanel() },
|
|
623
|
+
],
|
|
624
|
+
});
|
|
625
|
+
// 卸载时宿主会统一 dispose;也可手动 handle.dispose()
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
示例:`packages/plugins/presets/media-viewer`(缩放 / 全屏 Esc)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vetta-org/plugin-sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
24
|
+
"docs",
|
|
24
25
|
"src/tailwind-theme.css"
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
|
-
"clean": "rm -rf dist",
|
|
28
|
-
"
|
|
28
|
+
"clean": "rm -rf dist docs",
|
|
29
|
+
"bundle-docs": "node scripts/bundle-docs.mjs",
|
|
30
|
+
"build": "tsgo -p tsconfig.build.json && node scripts/bundle-docs.mjs",
|
|
31
|
+
"prepack": "node scripts/bundle-docs.mjs"
|
|
29
32
|
},
|
|
30
33
|
"dependencies": {
|
|
31
34
|
"@vetta-org/capability-sdk": "^0.1.0",
|