@xyz-agent/extension-protocol 0.1.1-dev.0

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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * GUI 渲染协议核心类型定义。
3
+ *
4
+ * GuiComponent 是 pi Component { render(width): string[] } 的可序列化镜像。
5
+ * extension 按 ctx.mode 分支:TUI 走原生 Component,RPC 走 GuiComponent(放进 details.__gui__)。
6
+ *
7
+ * GuiComponentProps 是类型路由的聚合点:通用布局原语 + extension 专属组件
8
+ * 全部在此声明键值,子类型直接内联本文件(纯类型,无运行时逻辑)。
9
+ *
10
+ * @see docs/architecture/extension-gui-protocol.md
11
+ */
12
+ declare const PROTOCOL_VERSION: 1;
13
+ /**
14
+ * GUI 渲染组件——pi Component 的可序列化镜像。
15
+ *
16
+ * pi: Component { render(width): string[] } ← ANSI 文本行
17
+ * gui: GuiComponent = { type, props } ← 结构化数据
18
+ */
19
+ interface GuiComponent<T extends GuiComponentType = GuiComponentType> {
20
+ /** 组件类型,前端按此路由到 Vue 组件 */
21
+ type: T;
22
+ /** 组件 props,类型由 type 决定 */
23
+ props: GuiComponentProps[T];
24
+ }
25
+ type GuiComponentType = keyof GuiComponentProps;
26
+ interface GuiComponentProps {
27
+ /** ANSI 文本兜底——保留原始 ANSI 序列,前端用 ansi_up 渲染 */
28
+ 'ansi-text': {
29
+ lines: string[];
30
+ };
31
+ /** 卡片容器——替代 TUI 的 ┌─┐││└─┘ box 边框 */
32
+ 'card': {
33
+ variant?: 'default' | 'elevated' | 'danger' | 'success';
34
+ header?: GuiComponent | string;
35
+ body: GuiComponent[];
36
+ };
37
+ /** 统计行——替代 TUI 的 "N turns · Nk · Ns" */
38
+ 'stats-line': {
39
+ items: StatItem[];
40
+ };
41
+ /** 进度条——替代 TUI 的 ████░░░░ */
42
+ 'progress-bar': {
43
+ label?: string;
44
+ current: number;
45
+ total: number;
46
+ unit?: string;
47
+ severity?: 'ok' | 'warn' | 'danger';
48
+ };
49
+ /** 列表树——替代 TUI 的 ⎿ ├─ └─ 缩进 */
50
+ 'list-tree': {
51
+ items: TreeItem[];
52
+ };
53
+ /** 双列网格——替代 TUI 的 │ 列分隔 */
54
+ 'columns': {
55
+ children: GuiComponent[];
56
+ ratios?: number[];
57
+ };
58
+ /** 标签栏——替代 TUI 的 tab │ 分隔 */
59
+ 'tab-bar': {
60
+ tabs: {
61
+ label: string;
62
+ active?: boolean;
63
+ status?: 'done' | 'pending';
64
+ }[];
65
+ };
66
+ /** 自定义组件——逃生口(仅限内置 extension 编译期注册) */
67
+ 'custom': {
68
+ component: string;
69
+ props: Record<string, unknown>;
70
+ };
71
+ }
72
+ interface GuiRenderResult {
73
+ /** 版本协商,前端检测,不认识降级 ansi-text */
74
+ v: typeof PROTOCOL_VERSION;
75
+ component: GuiComponent;
76
+ }
77
+ interface StatItem {
78
+ label?: string;
79
+ value: string;
80
+ severity?: 'ok' | 'warn' | 'danger';
81
+ icon?: string;
82
+ }
83
+ interface TreeItem {
84
+ icon?: TreeItemIcon;
85
+ label: string;
86
+ status?: 'running' | 'done' | 'failed';
87
+ depth?: number;
88
+ children?: TreeItem[];
89
+ }
90
+ type TreeItemIcon = 'arrow' | 'check' | 'cross' | 'circle' | 'dot' | 'pause' | 'branch';
91
+
92
+ /**
93
+ * 通用传输 marker。extension 用 guiSetWidget 编码进 string[],
94
+ * runtime event-adapter 检测 marker 解码为结构化 WS 帧。
95
+ *
96
+ * NUL 字符开头的 marker,不会出现在正常文本中。
97
+ */
98
+ declare const GUI_WIDGET_MARKER = "\0XYZ_GUI_WIDGET:";
99
+
100
+ /**
101
+ * helpers 需要的 ctx 最小接口。
102
+ * pi 的 ExtensionContext 天然满足此结构(有 mode/hasUI/ui 字段)。
103
+ * 用结构化类型而非 import pi SDK,保持协议包零依赖。
104
+ */
105
+ interface GuiContext {
106
+ mode: 'tui' | 'rpc' | 'json' | 'print';
107
+ hasUI: boolean;
108
+ ui?: {
109
+ setWidget?: (key: string, lines: string[] | undefined) => void;
110
+ select?: (header: string, options: string[], opts?: {
111
+ signal?: AbortSignal;
112
+ }) => Promise<string | undefined>;
113
+ input?: (header: string, prompt: string, opts?: {
114
+ signal?: AbortSignal;
115
+ }) => Promise<string | undefined>;
116
+ confirm?: (header: string, prompt: string, opts?: {
117
+ signal?: AbortSignal;
118
+ }) => Promise<boolean | undefined>;
119
+ custom?: (factory: unknown, opts?: unknown) => Promise<Record<string, string | string[]> | undefined>;
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Extension GUI 渲染协议 helper 函数(通用层)。
125
+ *
126
+ * 设计原则:
127
+ * - 零运行时依赖(不依赖 pi SDK)
128
+ * - helpers 接受最小化的 ctx 结构(结构化类型),pi 的 ExtensionContext 天然满足
129
+ * - extension 开发者只需调这些 helper,不需要了解底层编码细节
130
+ */
131
+
132
+ /**
133
+ * 检测当前环境是否支持 GUI 渲染(RPC 模式 = GUI 渲染通道有效)。
134
+ * TUI/json/print 模式走 pi 原生渲染,不需要 GuiComponent。
135
+ */
136
+ declare function isGuiCapable(ctx: GuiContext): boolean;
137
+ /**
138
+ * 构造 GuiRenderResult,放进 details.__gui__。
139
+ * stripUndefined 确保序列化不含 undefined(JSON.stringify 会丢弃 undefined 字段)。
140
+ */
141
+ declare function guiResult(component: GuiComponent): GuiRenderResult;
142
+ /**
143
+ * 构造 GuiComponent,带类型推断。
144
+ * 类型参数 T 约束 props 到对应类型的 props 形状。
145
+ */
146
+ declare function guiComponent<T extends GuiComponentType>(type: T, props: GuiComponentProps[T]): GuiComponent<T>;
147
+ /**
148
+ * 设置 GUI widget。RPC 模式下用 marker 编码 GuiComponent JSON 进 string[],
149
+ * runtime event-adapter 检测 marker 解码为结构化 WS 帧。
150
+ * TUI 模式下此函数无操作(extension 应在 TUI 分支调原生 ctx.ui.setWidget 传 Component factory)。
151
+ *
152
+ * 传 undefined 清除 widget。
153
+ */
154
+ declare function guiSetWidget(ctx: GuiContext, key: string, component: GuiComponent | undefined): void;
155
+ /**
156
+ * 从 details 中提取 GuiRenderResult。前端统一用此函数读取 __gui__,
157
+ * 集中校验版本号,避免散落的 as 断言。
158
+ */
159
+ declare function extractGui(details: Record<string, unknown> | undefined): GuiRenderResult | undefined;
160
+ /**
161
+ * 最小形状校验:判断 unknown 值是否为合法 GuiComponent(有 type 字符串 + props 对象)。
162
+ * 用于 widgetGui marker 解码后防止异常结构进入渲染层。
163
+ * 不校验 type 是否为已知值(GuiComponentRenderer 对未知 type 有 AnsiText 降级)。
164
+ */
165
+ declare function isGuiComponent(value: unknown): value is GuiComponent;
166
+
167
+ /**
168
+ * ask-user extension 的富交互类型定义。
169
+ *
170
+ * custom() 在 RPC 模式不可用(Component 是代码不是数据),ask-user 的「表单类」
171
+ * 交互改走 select 通道:askUserInteract() 把 AskUserQuestion[] 序列化进 select 的
172
+ * options[0],runtime event-adapter 检测 ASK_USER_MARKER 透传 questions,
173
+ * 前端 AskUserOverlay 渲染富交互 UI。
174
+ *
175
+ * 这是 ask-user 的定制协议,不是通用富交互协议。
176
+ * 设计参考 ask-user 的 Question 结构。
177
+ */
178
+ /**
179
+ * ask-user 富交互问题声明。
180
+ */
181
+ interface AskUserQuestion {
182
+ /** Tab 标签 / 简短标题。多问题时用于 tab 切换,≤12 字符。
183
+ * 可选——未提供时前端用 question 文本作为 tab 标签(前 12 字符截断显示)
184
+ * 和 answers key(完整 question 文本,不截断)。 */
185
+ header?: string;
186
+ /** 完整问题文本。也作为 answers 的 fallback key(header 缺失时) */
187
+ question: string;
188
+ /** 上下文摘要(可选)。显示在问题上方,帮用户理解背景 */
189
+ context?: string;
190
+ /** 互斥选项列表(可选)。无 options = 纯自由文本输入 */
191
+ options?: AskUserOption[];
192
+ /** 是否允许多选。仅 options 存在时有效 */
193
+ multiSelect?: boolean;
194
+ /** 是否允许自由文本输入(Other)。
195
+ * - 有 options 时:默认 true,前端在选项末尾追加 Other 输入框;设 false 则不追加
196
+ * - 无 options 时:整个问题就是自由输入,此字段被忽略 */
197
+ allowOther?: boolean;
198
+ /** 是否允许附加评论。选中后可追加短文本 */
199
+ allowComment?: boolean;
200
+ }
201
+ interface AskUserOption {
202
+ /** 显示标签 */
203
+ label: string;
204
+ /** 回传值。未提供时用 label */
205
+ value?: string;
206
+ /** 描述(可选)。显示在 label 下方,解释 tradeoff */
207
+ description?: string;
208
+ }
209
+ /**
210
+ * ask-user 富交互回传结果。key = question.header(header 缺失时用 question 文本)。
211
+ *
212
+ * 答案编码规则(避免逗号歧义):
213
+ * - 单选:value = 选中项的 value string(或 label)
214
+ * - 多选:value = JSON.stringify(选中项 value 数组),如 '["pg","mysql"]'
215
+ * (不用逗号 join——option value 可能含逗号导致 split 歧义)
216
+ * - Other 文本:单独 key `${header}__other`,value = 自由文本(不混进选中项数组)
217
+ * - comment:单独 key `${header}__comment`,value = 评论文本
218
+ *
219
+ * extension 解析示例:
220
+ * const selected = JSON.parse(answers[header]) // 多选 → string[]
221
+ * const other = answers[`${header}__other`] // Other 自由文本
222
+ * const comment = answers[`${header}__comment`] // 评论
223
+ */
224
+ type AskUserAnswers = Record<string, string>;
225
+
226
+ /**
227
+ * ask-user 富交互请求的 title marker。runtime event-adapter 和前端 useExtensionUI
228
+ * 检测此 marker 区分 ask-user 请求与普通 select。
229
+ *
230
+ * NUL 前缀确保不会与 extension 正常的 select title 冲突。
231
+ * 与 GUI_WIDGET_MARKER 同理(见 core/markers.ts)。
232
+ */
233
+ declare const ASK_USER_MARKER = "\0XYZ_ASK_USER";
234
+
235
+ /**
236
+ * ask-user extension 的富交互 helper(定制层)。
237
+ *
238
+ * askUserInteract() 是 ask-user extension 在 RPC 模式下的交互入口。
239
+ * TUI 模式下 extension 必须自行调 ctx.ui.custom()。
240
+ *
241
+ * 走 select 通道 + marker(ASK_USER_MARKER),复用 select 的全部管道逻辑
242
+ * (队列 / 超时 / 回传 / abort),零重复代码。
243
+ */
244
+
245
+ /**
246
+ * ask-user 富交互入口(RPC 模式专用)。
247
+ *
248
+ * RPC 模式:用 select 通道携带 questions 数据,前端渲染富交互 UI。
249
+ * TUI 模式:抛错。extension 必须自行调 ctx.ui.custom()。
250
+ *
251
+ * @param ctx ExtensionContext(pi 提供)
252
+ * @param questions 交互问题声明
253
+ * @param options 可选:signal(abort)、allowCancel(前端是否显示取消按钮)
254
+ * @returns answers(key=header/question, value=JSON编码的答案),用户取消返回 null
255
+ */
256
+ declare function askUserInteract(ctx: GuiContext, questions: AskUserQuestion[], options?: {
257
+ signal?: AbortSignal;
258
+ allowCancel?: boolean;
259
+ }): Promise<AskUserAnswers | null>;
260
+ /**
261
+ * 从 answers 中提取某个问题的选中值(单选返回 string,多选返回 string[])。
262
+ *
263
+ * 多选 answers 的 value 是 JSON.stringify(string[]),此 helper 自动 parse。
264
+ * parse 失败时降级返回 [raw](兼容非标准格式的回传)。
265
+ */
266
+ declare function getAskUserAnswer(answers: AskUserAnswers, question: AskUserQuestion): string | string[] | undefined;
267
+ /** 从 answers 中提取 Other 自由文本 */
268
+ declare function getAskUserOther(answers: AskUserAnswers, question: AskUserQuestion): string | undefined;
269
+ /** 从 answers 中提取评论 */
270
+ declare function getAskUserComment(answers: AskUserAnswers, question: AskUserQuestion): string | undefined;
271
+ /**
272
+ * 类型守卫:验证 unknown 是否为合法的 AskUserQuestion。
273
+ * 用于前端从 runtime 透传的 askUserQuestions(unknown[])中安全收窄。
274
+ */
275
+ declare function isAskUserQuestion(value: unknown): value is AskUserQuestion;
276
+
277
+ export { ASK_USER_MARKER, type AskUserAnswers, type AskUserOption, type AskUserQuestion, GUI_WIDGET_MARKER, type GuiComponent, type GuiComponentProps, type GuiComponentType, type GuiContext, type GuiRenderResult, PROTOCOL_VERSION, type StatItem, type TreeItem, type TreeItemIcon, askUserInteract, extractGui, getAskUserAnswer, getAskUserComment, getAskUserOther, guiComponent, guiResult, guiSetWidget, isAskUserQuestion, isGuiCapable, isGuiComponent };
package/dist/index.mjs ADDED
@@ -0,0 +1,126 @@
1
+ // src/core/types.ts
2
+ var PROTOCOL_VERSION = 1;
3
+
4
+ // src/core/markers.ts
5
+ var GUI_WIDGET_MARKER = "\0XYZ_GUI_WIDGET:";
6
+
7
+ // src/core/helpers.ts
8
+ function isGuiCapable(ctx) {
9
+ return ctx.mode === "rpc";
10
+ }
11
+ function guiResult(component) {
12
+ return {
13
+ v: PROTOCOL_VERSION,
14
+ component: stripUndefined(component)
15
+ };
16
+ }
17
+ function guiComponent(type, props) {
18
+ return { type, props };
19
+ }
20
+ function guiSetWidget(ctx, key, component) {
21
+ if (!ctx.ui?.setWidget) return;
22
+ if (component) {
23
+ const encoded = [GUI_WIDGET_MARKER + JSON.stringify(stripUndefined(component))];
24
+ ctx.ui.setWidget(key, encoded);
25
+ } else {
26
+ ctx.ui.setWidget(key, void 0);
27
+ }
28
+ }
29
+ function extractGui(details) {
30
+ const g = details?.__gui__;
31
+ if (g && typeof g === "object" && "v" in g && "component" in g && g.v === PROTOCOL_VERSION) {
32
+ return g;
33
+ }
34
+ return void 0;
35
+ }
36
+ function isGuiComponent(value) {
37
+ if (value === null || typeof value !== "object") return false;
38
+ const obj = value;
39
+ return typeof obj.type === "string" && obj.props !== null && obj.props !== void 0 && typeof obj.props === "object";
40
+ }
41
+ function stripUndefined(obj) {
42
+ if (obj === null || obj === void 0) return obj;
43
+ if (typeof obj !== "object") return obj;
44
+ if (Array.isArray(obj)) return obj.map(stripUndefined);
45
+ const result = {};
46
+ for (const [key, value] of Object.entries(obj)) {
47
+ if (value !== void 0) {
48
+ result[key] = typeof value === "object" ? stripUndefined(value) : value;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+
54
+ // src/extensions/ask-user/marker.ts
55
+ var ASK_USER_MARKER = "\0XYZ_ASK_USER";
56
+
57
+ // src/extensions/ask-user/helpers.ts
58
+ async function askUserInteract(ctx, questions, options) {
59
+ if (questions.length === 0) return {};
60
+ if (isGuiCapable(ctx) && ctx.ui?.select) {
61
+ const payload = JSON.stringify(stripUndefined({
62
+ questions,
63
+ allowCancel: options?.allowCancel ?? true
64
+ }));
65
+ const value = await ctx.ui.select(
66
+ ASK_USER_MARKER,
67
+ // title = marker,runtime/前端据此识别
68
+ [payload],
69
+ // options[0] = JSON payload(runtime 解析)
70
+ { signal: options?.signal }
71
+ );
72
+ if (value === void 0) return null;
73
+ try {
74
+ return JSON.parse(value);
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ throw new Error(
80
+ "askUserInteract() is only available in RPC mode. In TUI mode, use ctx.ui.custom() with your own Component directly."
81
+ );
82
+ }
83
+ function askUserKey(question) {
84
+ return question.header ?? question.question;
85
+ }
86
+ function getAskUserAnswer(answers, question) {
87
+ const key = askUserKey(question);
88
+ const raw = answers[key];
89
+ if (raw === void 0) return void 0;
90
+ if (question.multiSelect) {
91
+ try {
92
+ const parsed = JSON.parse(raw);
93
+ return Array.isArray(parsed) ? parsed : [raw];
94
+ } catch {
95
+ return [raw];
96
+ }
97
+ }
98
+ return raw;
99
+ }
100
+ function getAskUserOther(answers, question) {
101
+ return answers[`${askUserKey(question)}__other`];
102
+ }
103
+ function getAskUserComment(answers, question) {
104
+ return answers[`${askUserKey(question)}__comment`];
105
+ }
106
+ function isAskUserQuestion(value) {
107
+ if (typeof value !== "object" || value === null) return false;
108
+ const q = value;
109
+ return typeof q.question === "string" && (q.header === void 0 || typeof q.header === "string") && (q.options === void 0 || Array.isArray(q.options)) && (q.multiSelect === void 0 || typeof q.multiSelect === "boolean") && (q.allowOther === void 0 || typeof q.allowOther === "boolean") && (q.allowComment === void 0 || typeof q.allowComment === "boolean");
110
+ }
111
+ export {
112
+ ASK_USER_MARKER,
113
+ GUI_WIDGET_MARKER,
114
+ PROTOCOL_VERSION,
115
+ askUserInteract,
116
+ extractGui,
117
+ getAskUserAnswer,
118
+ getAskUserComment,
119
+ getAskUserOther,
120
+ guiComponent,
121
+ guiResult,
122
+ guiSetWidget,
123
+ isAskUserQuestion,
124
+ isGuiCapable,
125
+ isGuiComponent
126
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@xyz-agent/extension-protocol",
3
+ "version": "0.1.1-dev.0",
4
+ "description": "Extension GUI rendering protocol: types and helpers for pi extension dual-mode (TUI/GUI) rendering",
5
+ "main": "dist/index.mjs",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "license": "MIT",
19
+ "devDependencies": {
20
+ "tsup": "^8.5.1",
21
+ "typescript": "^5.7.0",
22
+ "vitest": "^4.1.6"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run"
28
+ }
29
+ }