@leeoohoo/ui-apps-devkit 0.1.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.
Files changed (55) hide show
  1. package/README.md +70 -0
  2. package/bin/chatos-uiapp.js +5 -0
  3. package/package.json +22 -0
  4. package/src/cli.js +53 -0
  5. package/src/commands/dev.js +14 -0
  6. package/src/commands/init.js +141 -0
  7. package/src/commands/install.js +55 -0
  8. package/src/commands/pack.js +72 -0
  9. package/src/commands/validate.js +103 -0
  10. package/src/lib/args.js +49 -0
  11. package/src/lib/config.js +29 -0
  12. package/src/lib/fs.js +78 -0
  13. package/src/lib/path-boundary.js +16 -0
  14. package/src/lib/plugin.js +45 -0
  15. package/src/lib/template.js +168 -0
  16. package/src/sandbox/server.js +861 -0
  17. package/templates/basic/README.md +58 -0
  18. package/templates/basic/chatos.config.json +5 -0
  19. package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +181 -0
  20. package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -0
  21. package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +123 -0
  22. package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +110 -0
  23. package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +227 -0
  24. package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -0
  25. package/templates/basic/plugin/apps/app/index.mjs +263 -0
  26. package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -0
  27. package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -0
  28. package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -0
  29. package/templates/basic/plugin/backend/index.mjs +37 -0
  30. package/templates/basic/template.json +7 -0
  31. package/templates/notepad/README.md +36 -0
  32. package/templates/notepad/chatos.config.json +4 -0
  33. package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +181 -0
  34. package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -0
  35. package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +123 -0
  36. package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +110 -0
  37. package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +227 -0
  38. package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -0
  39. package/templates/notepad/plugin/apps/app/api.mjs +30 -0
  40. package/templates/notepad/plugin/apps/app/dom.mjs +14 -0
  41. package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -0
  42. package/templates/notepad/plugin/apps/app/index.mjs +1056 -0
  43. package/templates/notepad/plugin/apps/app/layers.mjs +338 -0
  44. package/templates/notepad/plugin/apps/app/markdown.mjs +120 -0
  45. package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -0
  46. package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -0
  47. package/templates/notepad/plugin/apps/app/mcp-server.mjs +200 -0
  48. package/templates/notepad/plugin/apps/app/styles.mjs +355 -0
  49. package/templates/notepad/plugin/apps/app/tags.mjs +21 -0
  50. package/templates/notepad/plugin/apps/app/ui.mjs +280 -0
  51. package/templates/notepad/plugin/backend/index.mjs +99 -0
  52. package/templates/notepad/plugin/plugin.json +23 -0
  53. package/templates/notepad/plugin/shared/notepad-paths.mjs +62 -0
  54. package/templates/notepad/plugin/shared/notepad-store.mjs +765 -0
  55. package/templates/notepad/template.json +8 -0
@@ -0,0 +1,392 @@
1
+ # ChatOS UI Prompts(笑脸交互待办)协议
2
+
3
+ UI Prompts 是 ChatOS 的全局交互队列:任意组件(AI / MCP / UI Apps)都通过向队列写入 `request` 记录来发起一次“需要用户输入/确认”的交互;用户在 UI(右下角笑脸面板)完成填写后,系统写入对应的 `response` 记录来结束该交互。
4
+
5
+ 本协议定义:
6
+
7
+ - 存储格式:`ui-prompts.jsonl`(JSON Lines 追加日志)
8
+ - 交互生命周期:`request` → `response`
9
+ - UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm`)
10
+ - UI Apps 的 Host API 调用方式(`host.uiPrompts.*`)
11
+
12
+ 实现对照(以代码为准):
13
+
14
+ - UI 渲染:`deepseek_cli/apps/ui/src/features/session/floating-island/FloatingIslandPrompt.jsx`
15
+ - 笑脸面板:`deepseek_cli/apps/ui/src/components/UiPromptsSmileHub.jsx`
16
+ - Host IPC:`deepseek_cli/electron/main.js`(`uiPrompts:*`)
17
+ - 写入/读取日志:`aide/electron/session-api.js`(`requestUiPrompt` / `respondUiPrompt` / `readUiPromptsPayload`)
18
+ - MCP `ui_prompter`:`aide/mcp_servers/ui-prompt-server.js`
19
+
20
+ ---
21
+
22
+ ## 1. 存储:`ui-prompts.jsonl`
23
+
24
+ ### 1.1 文件位置
25
+
26
+ UI Prompts 以 JSONL 形式追加写入到:
27
+
28
+ - `stateDir/ui-prompts.jsonl`
29
+
30
+ 其中 `stateDir` 为 ChatOS 的状态目录(宿主按 `hostApp` 做隔离;ChatOS 的 `hostApp=chatos`)。
31
+
32
+ ### 1.2 JSONL 记录类型
33
+
34
+ 每一行是一个 JSON 对象(以下简称 entry)。当前 UI Prompts 只消费:
35
+
36
+ - `entry.type === "ui_prompt"`
37
+
38
+ 并按 `entry.action` 区分:
39
+
40
+ - `entry.action === "request"`:发起交互
41
+ - `entry.action === "response"`:结束交互
42
+
43
+ ### 1.3 Pending 判定(队列语义)
44
+
45
+ UI 端的“待处理”集合由日志推导:
46
+
47
+ - 对同一个 `requestId`,存在 `request` 但不存在 `response` 时,该交互处于 pending 状态;
48
+ - 当出现 `response` 后,该交互从 pending 中移除;
49
+ - 日志是追加写入,系统不删除旧行。
50
+
51
+ ---
52
+
53
+ ## 2. Host API(UI Apps 对接入口)
54
+
55
+ UI Apps 的 `module` 应用通过 Host API 与 UI Prompts 交互:
56
+
57
+ - `host.uiPrompts.read(): Promise<{ path: string, entries: any[] }>`
58
+ - `host.uiPrompts.onUpdate((payload) => void): () => void`
59
+ - `host.uiPrompts.request(payload): Promise<{ ok: true, requestId: string }>`
60
+ - `host.uiPrompts.respond(payload): Promise<{ ok: true }>`
61
+ - `host.uiPrompts.open()/close()/toggle()`
62
+
63
+ `host.uiPrompts.request()` 在 `prompt.source` 为空时,会自动写入 `${pluginId}:${appId}`。
64
+
65
+ ### 2.1 `host.uiPrompts.request(payload)`
66
+
67
+ `payload` 字段:
68
+
69
+ | 字段 | 类型 | 必填 | 说明 |
70
+ |---|---:|---:|---|
71
+ | `requestId` | `string` | 否 | 为空时宿主生成 |
72
+ | `runId` | `string` | 否 | 透传到 entry,用于 UI 标识来源 run |
73
+ | `prompt` | `object` | 是 | 交互定义(见第 4-8 节) |
74
+
75
+ 返回:
76
+
77
+ - `{ ok: true, requestId }`(`requestId` 为最终使用的 ID)
78
+
79
+ ### 2.2 `host.uiPrompts.respond(payload)`
80
+
81
+ `payload` 字段:
82
+
83
+ | 字段 | 类型 | 必填 | 说明 |
84
+ |---|---:|---:|---|
85
+ | `requestId` | `string` | 是 | 对应 `request` 的 `requestId` |
86
+ | `runId` | `string` | 否 | 透传到 entry |
87
+ | `response` | `object` | 是 | 必须包含 `response.status` |
88
+
89
+ 返回:
90
+
91
+ - `{ ok: true }`
92
+
93
+ ---
94
+
95
+ ## 3. Entry 协议(写入 `ui-prompts.jsonl` 的数据结构)
96
+
97
+ ### 3.1 Request Entry
98
+
99
+ ```json
100
+ {
101
+ "ts": "2026-01-11T00:00:00.000Z",
102
+ "type": "ui_prompt",
103
+ "action": "request",
104
+ "requestId": "uuid-or-app-generated-id",
105
+ "runId": "optional-run-id",
106
+ "prompt": { "...": "see below" }
107
+ }
108
+ ```
109
+
110
+ 字段:
111
+
112
+ - `ts`:ISO 时间字符串
113
+ - `type`:固定为 `"ui_prompt"`
114
+ - `action`:固定为 `"request"`
115
+ - `requestId`:字符串;同一次交互的唯一 ID
116
+ - `runId`:可选字符串;用于在 UI 中标识来源 run(显示为 Tag)
117
+ - `prompt`:对象;由 `prompt.kind` 决定结构(见第 4 节)
118
+
119
+ ### 3.2 Response Entry
120
+
121
+ ```json
122
+ {
123
+ "ts": "2026-01-11T00:00:10.000Z",
124
+ "type": "ui_prompt",
125
+ "action": "response",
126
+ "requestId": "uuid-or-app-generated-id",
127
+ "runId": "optional-run-id",
128
+ "response": { "...": "see below" }
129
+ }
130
+ ```
131
+
132
+ 字段:
133
+
134
+ - `ts`:ISO 时间字符串
135
+ - `type`:固定为 `"ui_prompt"`
136
+ - `action`:固定为 `"response"`
137
+ - `requestId`:必须与对应 `request` 一致
138
+ - `runId`:可选;用于标识来源 run
139
+ - `response`:对象;必须包含 `response.status`
140
+
141
+ ---
142
+
143
+ ## 4. Prompt 协议(`prompt.kind`)
144
+
145
+ ### 4.1 通用字段(所有 kind 均可出现)
146
+
147
+ | 字段 | 类型 | 必填 | 说明 |
148
+ |---|---:|---:|---|
149
+ | `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` |
150
+ | `title` | `string` | 否 | UI 标题 |
151
+ | `message` | `string` | 否 | UI 描述/说明 |
152
+ | `source` | `string` | 否 | 来源标识(UI 显示 Tag) |
153
+ | `allowCancel` | `boolean` | 否 | `false` 时 UI 不提供取消入口;其余情况允许取消 |
154
+
155
+ ---
156
+
157
+ ## 5. `kind="kv"`:键值表单(多字段输入)
158
+
159
+ ### 5.1 请求结构
160
+
161
+ ```json
162
+ {
163
+ "kind": "kv",
164
+ "title": "需要你补充信息",
165
+ "message": "请填写表单后继续。",
166
+ "source": "com.example.plugin:my-app",
167
+ "allowCancel": true,
168
+ "fields": [
169
+ {
170
+ "key": "name",
171
+ "label": "姓名",
172
+ "description": "用于报告署名",
173
+ "placeholder": "请输入",
174
+ "default": "",
175
+ "required": true,
176
+ "multiline": false,
177
+ "secret": false
178
+ }
179
+ ]
180
+ }
181
+ ```
182
+
183
+ 字段约束:
184
+
185
+ - `fields`:数组,长度 `1..50`
186
+ - `fields[].key`:字符串,非空且在 `fields` 内唯一
187
+ - `fields[].label/description/placeholder/default`:可选字符串
188
+ - `fields[].required/multiline/secret`:可选布尔
189
+
190
+ UI 渲染规则:
191
+
192
+ - `multiline=true` → 多行输入框(TextArea)
193
+ - `secret=true` → 密码输入框(Password)
194
+ - 否则 → 单行输入框(Input)
195
+
196
+ ### 5.2 响应结构
197
+
198
+ ```json
199
+ { "status": "ok", "values": { "name": "Alice" } }
200
+ ```
201
+
202
+ - `status="ok"` 时,`values` 必须为对象,value 类型为 `string`
203
+ - `status!="ok"` 时,`values` 字段不参与消费
204
+
205
+ ---
206
+
207
+ ## 6. `kind="choice"`:单选 / 多选
208
+
209
+ ### 6.1 请求结构(单选)
210
+
211
+ ```json
212
+ {
213
+ "kind": "choice",
214
+ "title": "需要你做出选择",
215
+ "message": "请选择一项。",
216
+ "source": "com.example.plugin:my-app",
217
+ "allowCancel": true,
218
+ "multiple": false,
219
+ "options": [
220
+ { "value": "alpha", "label": "Alpha", "description": "选项说明" },
221
+ { "value": "beta", "label": "Beta", "description": "" }
222
+ ],
223
+ "default": "alpha",
224
+ "minSelections": 0,
225
+ "maxSelections": 2
226
+ }
227
+ ```
228
+
229
+ ### 6.2 请求结构(多选)
230
+
231
+ ```json
232
+ {
233
+ "kind": "choice",
234
+ "title": "需要你做出选择(多选)",
235
+ "message": "请选择 1-2 项。",
236
+ "allowCancel": true,
237
+ "multiple": true,
238
+ "options": [
239
+ { "value": "a", "label": "A" },
240
+ { "value": "b", "label": "B" },
241
+ { "value": "c", "label": "C" }
242
+ ],
243
+ "default": ["a"],
244
+ "minSelections": 1,
245
+ "maxSelections": 2
246
+ }
247
+ ```
248
+
249
+ 字段约束:
250
+
251
+ - `options`:数组,长度 `1..60`
252
+ - `options[].value`:字符串,非空且在 `options` 内唯一
253
+ - `options[].label/description`:可选字符串
254
+ - `multiple`:布尔,缺省为 `false`
255
+ - `default`:
256
+ - `multiple=false` → `string`
257
+ - `multiple=true` → `string[]`
258
+ - 默认值必须来自 `options[].value`
259
+ - `minSelections/maxSelections`:
260
+ - `multiple=true` 时生效
261
+ - `minSelections`:整数,范围 `0..options.length`
262
+ - `maxSelections`:整数,范围 `1..options.length`
263
+ - `minSelections <= maxSelections`
264
+
265
+ ### 6.3 响应结构
266
+
267
+ 单选:
268
+
269
+ ```json
270
+ { "status": "ok", "selection": "alpha" }
271
+ ```
272
+
273
+ 多选:
274
+
275
+ ```json
276
+ { "status": "ok", "selection": ["a", "b"] }
277
+ ```
278
+
279
+ `status!="ok"` 时,`selection` 字段不参与消费。
280
+
281
+ ---
282
+
283
+ ## 7. `kind="task_confirm"`:任务创建确认(复杂表单)
284
+
285
+ 该类型用于“任务列表的创建/编辑/排序确认”。UI 会渲染一组可编辑任务卡片,并在提交时返回任务数组。
286
+
287
+ ### 7.1 请求结构
288
+
289
+ ```json
290
+ {
291
+ "kind": "task_confirm",
292
+ "title": "任务创建确认",
293
+ "message": "请确认任务列表。",
294
+ "allowCancel": true,
295
+ "source": "main",
296
+ "tasks": [
297
+ {
298
+ "draftId": "uuid",
299
+ "title": "写文档",
300
+ "details": "补齐 UI Prompts 协议",
301
+ "priority": "high",
302
+ "status": "todo",
303
+ "tags": ["docs", "ui-prompts"]
304
+ }
305
+ ],
306
+ "defaultRemark": ""
307
+ }
308
+ ```
309
+
310
+ 字段:
311
+
312
+ - `tasks`:可选数组(缺省为 `[]`)
313
+ - `tasks[].draftId`:字符串;为空时系统会生成
314
+ - `tasks[].title/details`:字符串
315
+ - `tasks[].priority`:`high | medium | low`(缺省为 `medium`)
316
+ - `tasks[].status`:`todo | doing | blocked | done`(缺省为 `todo`)
317
+ - `tasks[].tags`:`string[]`
318
+ - `defaultRemark`:可选字符串;用于初始化备注输入框
319
+
320
+ ### 7.2 响应结构
321
+
322
+ ```json
323
+ {
324
+ "status": "ok",
325
+ "tasks": [
326
+ {
327
+ "draftId": "uuid",
328
+ "title": "写文档",
329
+ "details": "补齐 UI Prompts 协议",
330
+ "priority": "high",
331
+ "status": "todo",
332
+ "tags": ["docs", "ui-prompts"]
333
+ }
334
+ ],
335
+ "remark": "按这个列表创建即可"
336
+ }
337
+ ```
338
+
339
+ `status="ok"` 时,`tasks` 为数组;`remark` 为可选字符串。
340
+ `status!="ok"` 时,`tasks` 字段不参与消费;`remark` 可出现。
341
+
342
+ ---
343
+
344
+ ## 8. `kind="file_change_confirm"`:文件变更确认(复杂表单)
345
+
346
+ 该类型用于“文件变更/命令执行前确认”。UI 会渲染 diff/命令与路径信息,并在提交时返回 `remark`。
347
+
348
+ ### 8.1 请求结构
349
+
350
+ ```json
351
+ {
352
+ "kind": "file_change_confirm",
353
+ "title": "文件变更确认",
354
+ "message": "即将写入文件。",
355
+ "allowCancel": true,
356
+ "source": "filesystem/write_file",
357
+ "path": "src/app.js",
358
+ "command": "node scripts/generate.js",
359
+ "cwd": "C:\\\\project\\\\aide",
360
+ "diff": "--- a/src/app.js\\n+++ b/src/app.js\\n@@ ...",
361
+ "defaultRemark": ""
362
+ }
363
+ ```
364
+
365
+ 字段:
366
+
367
+ - `path/command/cwd/diff/defaultRemark`:可选字符串
368
+
369
+ ### 8.2 响应结构
370
+
371
+ ```json
372
+ { "status": "ok", "remark": "继续执行" }
373
+ ```
374
+
375
+ `remark` 为可选字符串;`status="ok"` 表示确认继续,其它 `status` 表示取消/中止。
376
+
377
+ ---
378
+
379
+ ## 9. 复杂交互的构建方式
380
+
381
+ UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `request/response` 串联构成:
382
+
383
+ 1) 应用写入第 1 条 `request`(得到 `requestId`)
384
+ 2) 应用监听 `host.uiPrompts.onUpdate`,在 entries 中定位同 `requestId` 的 `response`
385
+ 3) 应用基于该 `response` 决定下一步,再写入下一条 `request`
386
+
387
+ 在该模型下:
388
+
389
+ - “多字段表单”使用 `kind="kv"` 的 `fields[]` 承载
390
+ - “多选/单选”使用 `kind="choice"` 的 `multiple/options` 承载
391
+ - “任务列表确认”使用 `kind="task_confirm"` 承载
392
+ - “diff/命令确认”使用 `kind="file_change_confirm"` 承载
@@ -0,0 +1,30 @@
1
+ function ensureBridgeAvailable(bridgeEnabled) {
2
+ if (!bridgeEnabled) {
3
+ throw new Error('Host bridge not available (backend.invoke disabled)');
4
+ }
5
+ }
6
+
7
+ export function createNotesApi({ host, bridgeEnabled }) {
8
+ if (!host || typeof host !== 'object') throw new Error('host is required');
9
+
10
+ const invoke = async (method, params) => {
11
+ ensureBridgeAvailable(bridgeEnabled);
12
+ return await host.backend.invoke(method, params);
13
+ };
14
+
15
+ return {
16
+ init: async () => await invoke('notes.init'),
17
+ listFolders: async () => await invoke('notes.listFolders'),
18
+ createFolder: async (params) => await invoke('notes.createFolder', params),
19
+ renameFolder: async (params) => await invoke('notes.renameFolder', params),
20
+ deleteFolder: async (params) => await invoke('notes.deleteFolder', params),
21
+ listNotes: async (params) => await invoke('notes.listNotes', params),
22
+ createNote: async (params) => await invoke('notes.createNote', params),
23
+ getNote: async (params) => await invoke('notes.getNote', params),
24
+ updateNote: async (params) => await invoke('notes.updateNote', params),
25
+ deleteNote: async (params) => await invoke('notes.deleteNote', params),
26
+ listTags: async () => await invoke('notes.listTags'),
27
+ searchNotes: async (params) => await invoke('notes.searchNotes', params),
28
+ };
29
+ }
30
+
@@ -0,0 +1,14 @@
1
+ export function normalizeString(value) {
2
+ return typeof value === 'string' ? value.trim() : '';
3
+ }
4
+
5
+ export function isElement(node) {
6
+ return node && typeof node === 'object' && typeof node.appendChild === 'function';
7
+ }
8
+
9
+ export function setButtonEnabled(btn, enabled) {
10
+ if (!btn) return;
11
+ btn.disabled = !enabled;
12
+ btn.dataset.disabled = enabled ? '0' : '1';
13
+ }
14
+
@@ -0,0 +1,35 @@
1
+ async function importFirst(candidates) {
2
+ const list = Array.isArray(candidates) ? candidates : [];
3
+ let lastError = null;
4
+ for (const candidate of list) {
5
+ if (!candidate) continue;
6
+ try {
7
+ return await import(candidate);
8
+ } catch (err) {
9
+ lastError = err;
10
+ }
11
+ }
12
+ const hint = list.filter(Boolean).join('\n - ');
13
+ const message = `Failed to load ds-tree modules. Tried:\n - ${hint}`;
14
+ const error = new Error(message);
15
+ error.cause = lastError;
16
+ throw error;
17
+ }
18
+
19
+ function makeCandidates(relativePath) {
20
+ const rel = typeof relativePath === 'string' ? relativePath.trim() : '';
21
+ if (!rel) return [];
22
+ return [
23
+ new URL(`../../common/ui/${rel}`, import.meta.url).toString(),
24
+ new URL(`../../../../../common/ui/${rel}`, import.meta.url).toString(),
25
+ ];
26
+ }
27
+
28
+ const [treeView, treeStyles] = await Promise.all([
29
+ importFirst(makeCandidates('ds-tree-view.mjs')),
30
+ importFirst(makeCandidates('ds-tree-styles.mjs')),
31
+ ]);
32
+
33
+ export const createDsPathTreeView = treeView.createDsPathTreeView;
34
+ export const DS_TREE_STYLES = treeStyles.DS_TREE_STYLES;
35
+