@leeoohoo/ui-apps-devkit 0.1.6 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeoohoo/ui-apps-devkit",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "ChatOS UI Apps DevKit (CLI + templates + sandbox) for building installable ChatOS UI Apps plugins.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,11 +1,29 @@
1
+ import fs from 'fs';
1
2
  import os from 'os';
2
3
  import path from 'path';
4
+ import crypto from 'crypto';
3
5
 
4
6
  import { copyDir, ensureDir, isDirectory, rmForce, sanitizeDirComponent } from '../lib/fs.js';
5
7
  import { loadDevkitConfig } from '../lib/config.js';
6
8
  import { findPluginDir, loadPluginManifest } from '../lib/plugin.js';
7
9
  import { STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
8
10
 
11
+ function createActionId(prefix) {
12
+ const base = typeof prefix === 'string' && prefix.trim() ? prefix.trim() : 'action';
13
+ const short = crypto.randomUUID().split('-')[0];
14
+ return `${base}-${Date.now().toString(36)}-${short}`;
15
+ }
16
+
17
+ function logWith(filePath, entry) {
18
+ if (!filePath) return;
19
+ try {
20
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
21
+ fs.appendFileSync(filePath, `${JSON.stringify(entry)}\n`, 'utf8');
22
+ } catch {
23
+ // ignore logging failures
24
+ }
25
+ }
26
+
9
27
  function defaultStateDir(hostApp) {
10
28
  const app = typeof hostApp === 'string' && hostApp.trim() ? hostApp.trim() : 'chatos';
11
29
  return path.join(os.homedir(), STATE_ROOT_DIRNAME, app);
@@ -28,29 +46,59 @@ function copyPluginDir(srcDir, destDir) {
28
46
 
29
47
  export async function cmdInstall({ flags }) {
30
48
  const { config } = loadDevkitConfig(process.cwd());
31
- const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
32
- const { pluginId, name, version } = loadPluginManifest(pluginDir);
33
-
49
+ const actionId = createActionId('devkit_install');
34
50
  const hostApp = String(flags['host-app'] || flags.hostApp || 'chatos').trim() || 'chatos';
35
51
  const stateDir = String(flags['state-dir'] || flags.stateDir || defaultStateDir(hostApp)).trim();
36
- if (!stateDir) throw new Error('stateDir is required');
52
+ const logFile = String(flags['log-file'] || flags.logFile || path.join(stateDir, 'devkit-install-log.jsonl')).trim();
53
+ const log = (level, message, meta, err) => {
54
+ const entry = {
55
+ ts: new Date().toISOString(),
56
+ level,
57
+ message,
58
+ actionId,
59
+ pid: process.pid,
60
+ meta: meta && typeof meta === 'object' ? meta : meta === undefined ? undefined : { value: meta },
61
+ error: err?.message || (err ? String(err) : undefined),
62
+ };
63
+ logWith(logFile, entry);
64
+ };
65
+
66
+ try {
67
+ if (!stateDir) throw new Error('stateDir is required');
68
+ log('info', 'devkit.install.start', { hostApp, stateDir, logFile });
69
+ const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
70
+ const { pluginId, name, version } = loadPluginManifest(pluginDir);
71
+
72
+ const pluginsRoot = path.join(stateDir, 'ui_apps', 'plugins');
73
+ ensureDir(pluginsRoot);
37
74
 
38
- const pluginsRoot = path.join(stateDir, 'ui_apps', 'plugins');
39
- ensureDir(pluginsRoot);
75
+ const dirName = sanitizeDirComponent(pluginId);
76
+ if (!dirName) throw new Error(`Invalid plugin id: ${pluginId}`);
40
77
 
41
- const dirName = sanitizeDirComponent(pluginId);
42
- if (!dirName) throw new Error(`Invalid plugin id: ${pluginId}`);
78
+ const destDir = path.join(pluginsRoot, dirName);
79
+ const replaced = isDirectory(destDir);
80
+ copyPluginDir(pluginDir, destDir);
43
81
 
44
- const destDir = path.join(pluginsRoot, dirName);
45
- const replaced = isDirectory(destDir);
46
- copyPluginDir(pluginDir, destDir);
82
+ log('info', 'devkit.install.complete', {
83
+ pluginId,
84
+ name,
85
+ version,
86
+ pluginDir,
87
+ destDir,
88
+ replaced,
89
+ });
47
90
 
48
- // eslint-disable-next-line no-console
49
- console.log(
50
- `Installed: ${pluginId} (${name}@${version})\n` +
51
- ` -> ${destDir}\n` +
52
- ` replaced: ${replaced}\n\n` +
53
- `Open ChatOS -> 应用 -> 刷新(同 id 覆盖生效)。`
54
- );
91
+ // eslint-disable-next-line no-console
92
+ console.log(
93
+ `Installed: ${pluginId} (${name}@${version})\n` +
94
+ ` -> ${destDir}\n` +
95
+ ` replaced: ${replaced}\n` +
96
+ ` log: ${logFile}\n\n` +
97
+ `Open ChatOS -> 应用 -> 刷新(同 id 覆盖生效)。`
98
+ );
99
+ } catch (err) {
100
+ log('error', 'devkit.install.failed', {}, err);
101
+ throw err;
102
+ }
55
103
  }
56
104
 
@@ -18,11 +18,52 @@ function statSizeSafe(filePath) {
18
18
  }
19
19
  }
20
20
 
21
+ function collectSymlinks(rootDir) {
22
+ const root = typeof rootDir === 'string' ? rootDir.trim() : '';
23
+ if (!root) return [];
24
+ const out = [];
25
+ const walk = (dir) => {
26
+ let entries = [];
27
+ try {
28
+ entries = fs.readdirSync(dir, { withFileTypes: true });
29
+ } catch {
30
+ return;
31
+ }
32
+ entries.forEach((entry) => {
33
+ if (!entry || !entry.name) return;
34
+ const fullPath = path.join(dir, entry.name);
35
+ let stat;
36
+ try {
37
+ stat = fs.lstatSync(fullPath);
38
+ } catch {
39
+ return;
40
+ }
41
+ if (stat.isSymbolicLink()) {
42
+ out.push(fullPath);
43
+ return;
44
+ }
45
+ if (stat.isDirectory()) {
46
+ walk(fullPath);
47
+ }
48
+ });
49
+ };
50
+ walk(root);
51
+ return out;
52
+ }
53
+
21
54
  export async function cmdValidate({ flags }) {
22
55
  const { config } = loadDevkitConfig(process.cwd());
23
56
  const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
24
57
 
25
58
  const { manifestPath, manifest } = loadPluginManifest(pluginDir);
59
+ const warnings = [];
60
+ const warn = (message) => warnings.push(message);
61
+
62
+ const symlinks = collectSymlinks(pluginDir);
63
+ if (symlinks.length > 0) {
64
+ const rel = symlinks.map((entry) => path.relative(pluginDir, entry) || entry);
65
+ warn(`Symlink detected inside plugin dir (may bypass path boundaries): ${rel.join(', ')}`);
66
+ }
26
67
 
27
68
  const manifestSize = statSizeSafe(manifestPath);
28
69
  assert(manifestSize <= 256 * 1024, `plugin.json too large (>256KiB): ${manifestSize} bytes`);
@@ -108,6 +149,13 @@ export async function cmdValidate({ flags }) {
108
149
  }
109
150
  }
110
151
 
152
+ if (warnings.length > 0) {
153
+ warnings.forEach((message) => {
154
+ // eslint-disable-next-line no-console
155
+ console.warn(`WARN: ${message}`);
156
+ });
157
+ }
158
+
111
159
  // eslint-disable-next-line no-console
112
160
  console.log(`OK: ${path.relative(process.cwd(), pluginDir)} (apps=${apps.length})`);
113
161
  }
@@ -668,10 +668,12 @@ function htmlPage() {
668
668
  border-radius:14px;
669
669
  overflow:hidden;
670
670
  box-shadow: 0 18px 60px rgba(0,0,0,0.18);
671
+ z-index: 1305;
672
+ pointer-events: auto;
671
673
  }
672
674
  #promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
673
675
  #promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
674
- #promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; }
676
+ #promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; z-index: 1305; pointer-events: auto; }
675
677
  .card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
676
678
  .row { display:flex; gap:10px; }
677
679
  .toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
@@ -867,8 +869,8 @@ function htmlPage() {
867
869
  <div class="card">
868
870
  <div class="section-title">Paths</div>
869
871
  <pre id="mcpPaths" class="mono"></pre>
870
- <label for="mcpWorkdir">Workdir Override (optional)</label>
871
- <input id="mcpWorkdir" type="text" placeholder="留空则使用默认 workdir" />
872
+ <label for="mcpWorkdir">ProjectRoot Override (optional)</label>
873
+ <input id="mcpWorkdir" type="text" placeholder="留空则使用默认 projectRoot" />
872
874
  </div>
873
875
  <div class="card">
874
876
  <div class="section-title">Model</div>
@@ -1146,9 +1148,15 @@ const appendMcpOutput = (label, payload) => {
1146
1148
  mcpOutput.scrollTop = mcpOutput.scrollHeight;
1147
1149
  };
1148
1150
 
1151
+ const resolveMcpPaths = () => {
1152
+ const projectRootOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
1153
+ if (!projectRootOverride) return sandboxPaths;
1154
+ return { ...sandboxPaths, projectRoot: projectRootOverride };
1155
+ };
1156
+
1149
1157
  const refreshMcpPaths = () => {
1150
1158
  if (!mcpPaths) return;
1151
- mcpPaths.textContent = formatJson(sandboxPaths);
1159
+ mcpPaths.textContent = formatJson(resolveMcpPaths());
1152
1160
  };
1153
1161
 
1154
1162
  const refreshMcpConfigHint = async () => {
@@ -1193,9 +1201,11 @@ const runMcpTest = async () => {
1193
1201
  systemPrompt: mcpSystem ? String(mcpSystem.value || '').trim() : '',
1194
1202
  disableTools: Boolean(mcpDisableTools?.checked),
1195
1203
  };
1196
- const workdirOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
1197
- if (workdirOverride) {
1198
- payload.callMeta = { workdir: workdirOverride };
1204
+ const projectRootOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
1205
+ if (projectRootOverride) {
1206
+ payload.callMeta = {
1207
+ chatos: { uiApp: { projectRoot: projectRootOverride } },
1208
+ };
1199
1209
  }
1200
1210
  setMcpStatus('Sending...');
1201
1211
  appendMcpOutput('request', payload);
@@ -1327,6 +1337,7 @@ if (btnLlmClear) btnLlmClear.addEventListener('click', () => saveLlmConfig({ cle
1327
1337
  if (btnMcpTest) btnMcpTest.addEventListener('click', () => setMcpPanelOpen(!isMcpPanelOpen()));
1328
1338
  if (btnHalfApp) btnHalfApp.addEventListener('click', () => toggleCompactSurface());
1329
1339
  if (btnMcpClose) btnMcpClose.addEventListener('click', () => setMcpPanelOpen(false));
1340
+ if (mcpWorkdir) mcpWorkdir.addEventListener('input', () => refreshMcpPaths());
1330
1341
  if (btnMcpClear)
1331
1342
  btnMcpClear.addEventListener('click', () => {
1332
1343
  if (mcpOutput) mcpOutput.textContent = '';
@@ -1396,6 +1407,7 @@ function renderPrompts() {
1396
1407
  form.style.gap = '10px';
1397
1408
 
1398
1409
  const kind = String(req?.prompt?.kind || '');
1410
+ const allowCancel = req?.prompt?.allowCancel !== false;
1399
1411
 
1400
1412
  const mkBtn = (label, danger) => {
1401
1413
  const btn = document.createElement('button');
@@ -1410,7 +1422,31 @@ function renderPrompts() {
1410
1422
  emitUpdate();
1411
1423
  };
1412
1424
 
1413
- if (kind === 'kv') {
1425
+ if (kind === 'result') {
1426
+ const markdownText =
1427
+ typeof req?.prompt?.markdown === 'string'
1428
+ ? req.prompt.markdown
1429
+ : typeof req?.prompt?.result === 'string'
1430
+ ? req.prompt.result
1431
+ : typeof req?.prompt?.content === 'string'
1432
+ ? req.prompt.content
1433
+ : '';
1434
+ const markdown = document.createElement('pre');
1435
+ markdown.className = 'mono';
1436
+ markdown.textContent = markdownText || '(无结果内容)';
1437
+ form.appendChild(markdown);
1438
+ const row = document.createElement('div');
1439
+ row.className = 'row';
1440
+ const ok = mkBtn('OK');
1441
+ ok.addEventListener('click', () => submit({ status: 'ok' }));
1442
+ row.appendChild(ok);
1443
+ if (allowCancel) {
1444
+ const cancel = mkBtn('Cancel', true);
1445
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1446
+ row.appendChild(cancel);
1447
+ }
1448
+ form.appendChild(row);
1449
+ } else if (kind === 'kv') {
1414
1450
  const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
1415
1451
  const values = {};
1416
1452
  for (const f of fields) {
@@ -1432,10 +1468,12 @@ function renderPrompts() {
1432
1468
  row.className = 'row';
1433
1469
  const ok = mkBtn('Submit');
1434
1470
  ok.addEventListener('click', () => submit({ status: 'ok', values }));
1435
- const cancel = mkBtn('Cancel', true);
1436
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1437
1471
  row.appendChild(ok);
1438
- row.appendChild(cancel);
1472
+ if (allowCancel) {
1473
+ const cancel = mkBtn('Cancel', true);
1474
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1475
+ row.appendChild(cancel);
1476
+ }
1439
1477
  form.appendChild(row);
1440
1478
  } else if (kind === 'choice') {
1441
1479
  const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
@@ -1464,20 +1502,24 @@ function renderPrompts() {
1464
1502
  row.className = 'row';
1465
1503
  const ok = mkBtn('Submit');
1466
1504
  ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
1467
- const cancel = mkBtn('Cancel', true);
1468
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1469
1505
  row.appendChild(ok);
1470
- row.appendChild(cancel);
1506
+ if (allowCancel) {
1507
+ const cancel = mkBtn('Cancel', true);
1508
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1509
+ row.appendChild(cancel);
1510
+ }
1471
1511
  form.appendChild(row);
1472
1512
  } else {
1473
1513
  const row = document.createElement('div');
1474
1514
  row.className = 'row';
1475
1515
  const ok = mkBtn('OK');
1476
1516
  ok.addEventListener('click', () => submit({ status: 'ok' }));
1477
- const cancel = mkBtn('Cancel', true);
1478
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1479
1517
  row.appendChild(ok);
1480
- row.appendChild(cancel);
1518
+ if (allowCancel) {
1519
+ const cancel = mkBtn('Cancel', true);
1520
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1521
+ row.appendChild(cancel);
1522
+ }
1481
1523
  form.appendChild(row);
1482
1524
  }
1483
1525
 
@@ -34,6 +34,7 @@ npm run dev
34
34
 
35
35
  - `plugin/plugin.json`:`apps[i].entry.type` 必须是 `module`,且 `path` 在插件目录内。
36
36
  - `plugin/plugin.json`:可选 `apps[i].entry.compact.path`,用于 compact UI。
37
+ - 安全:插件目录内尽量避免 symlink(`npm run validate` 会提示),以免路径边界与打包行为不一致。
37
38
  - `mount()`:返回卸载函数并清理事件/订阅;滚动放在应用内部,固定内容用 `slots.header`。
38
39
  - 主题:用 `host.theme.*` 与 `--ds-*` tokens;避免硬编码颜色。
39
40
  - 宿主能力:先判断 `host.bridge.enabled`,非宿主环境要可降级运行。
@@ -49,6 +50,7 @@ npm run dev
49
50
  2) **后端调用 LLM**:在 `plugin/backend/index.mjs` 里通过 `ctx.llm.complete()` 调用模型;前端用 `host.backend.invoke('llmComplete', { input })` 触发。
50
51
 
51
52
  说明:本地沙箱默认是 mock;可通过右上角 `AI Config` 配置 `API Key / Base URL / Model ID` 后启用真实模型调用,并用于测试应用 MCP(需配置 `ai.mcp`)。
53
+ 补充:沙箱默认把 `_meta.workdir` 设为 `dataDir`;如需模拟其它工作目录,可在 `AI Config` 里设置 `Workdir` 覆盖(支持 `$dataDir/$pluginDir/$projectRoot`)。
52
54
 
53
55
  ## 安装到本机 ChatOS
54
56
 
@@ -85,6 +85,8 @@
85
85
 
86
86
  同时,宿主默认注入 `_meta.workdir`(默认等于 `dataDir`;如需指定其它目录,可在 `ai.mcp.callMeta.workdir` 覆盖)。
87
87
 
88
+ > DevKit sandbox:右上角 `AI Config` 支持设置 `Workdir` 覆盖;留空即使用 `dataDir`(也支持 `$dataDir/$pluginDir/$projectRoot`)。
89
+
88
90
  示例(`plugin.json`):
89
91
 
90
92
  ```json
@@ -6,7 +6,7 @@ UI Prompts 是 ChatOS 的全局交互队列:任意组件(AI / MCP / UI Apps
6
6
 
7
7
  - 存储格式:`ui-prompts.jsonl`(JSON Lines 追加日志)
8
8
  - 交互生命周期:`request` → `response`
9
- - UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm`)
9
+ - UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result`)
10
10
  - UI Apps 的 Host API 调用方式(`host.uiPrompts.*`)
11
11
 
12
12
  实现对照(以代码为准):
@@ -146,7 +146,7 @@ UI Apps 的 `module` 应用通过 Host API 与 UI Prompts 交互:
146
146
 
147
147
  | 字段 | 类型 | 必填 | 说明 |
148
148
  |---|---:|---:|---|
149
- | `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` |
149
+ | `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result` |
150
150
  | `title` | `string` | 否 | UI 标题 |
151
151
  | `message` | `string` | 否 | UI 描述/说明 |
152
152
  | `source` | `string` | 否 | 来源标识(UI 显示 Tag) |
@@ -376,7 +376,36 @@ UI 渲染规则:
376
376
 
377
377
  ---
378
378
 
379
- ## 9. 复杂交互的构建方式
379
+ ## 9. `kind="result"`:执行结果(Markdown 展示)
380
+
381
+ 该类型用于“执行完成后的结果通知”。UI 会以 Markdown 形式展示结果,并在用户确认后写入 `response`。
382
+
383
+ ### 9.1 请求结构
384
+
385
+ ```json
386
+ {
387
+ "kind": "result",
388
+ "title": "执行结果",
389
+ "message": "已完成全部步骤。",
390
+ "source": "com.example.plugin:my-app",
391
+ "allowCancel": true,
392
+ "markdown": "## Done\n- step 1\n- step 2"
393
+ }
394
+ ```
395
+
396
+ 字段:
397
+
398
+ - `markdown`:可选字符串;作为 Markdown 内容展示
399
+
400
+ ### 9.2 响应结构
401
+
402
+ ```json
403
+ { "status": "ok" }
404
+ ```
405
+
406
+ ---
407
+
408
+ ## 10. 复杂交互的构建方式
380
409
 
381
410
  UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `request/response` 串联构成:
382
411
 
@@ -390,3 +419,4 @@ UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `re
390
419
  - “多选/单选”使用 `kind="choice"` 的 `multiple/options` 承载
391
420
  - “任务列表确认”使用 `kind="task_confirm"` 承载
392
421
  - “diff/命令确认”使用 `kind="file_change_confirm"` 承载
422
+ - “执行结果通知”使用 `kind="result"` 承载
@@ -9,6 +9,11 @@ npm install
9
9
  npm run dev
10
10
  ```
11
11
 
12
+ ## 本地沙箱 AI Config(可选)
13
+
14
+ - 右上角 `AI Config` 可配置 `API Key / Base URL / Model ID`,用于测试真实模型调用(以及 MCP 调用)。
15
+ - 沙箱默认把 `_meta.workdir` 设为 `dataDir`;如需模拟其它工作目录,可在 `AI Config` 里设置 `Workdir` 覆盖(支持 `$dataDir/$pluginDir/$projectRoot`)。
16
+
12
17
  ## 目录说明
13
18
 
14
19
  - `plugin/plugin.json`:插件清单(应用列表、入口、后端、AI 贡献)
@@ -29,6 +34,7 @@ npm run dev
29
34
 
30
35
  - `plugin/plugin.json`:`apps[i].entry.type` 必须是 `module`,且 `path` 在插件目录内。
31
36
  - `plugin/plugin.json`:可选 `apps[i].entry.compact.path`,用于 compact UI。
37
+ - 安全:插件目录内尽量避免 symlink(`npm run validate` 会提示),以免路径边界与打包行为不一致。
32
38
  - `mount()`:返回卸载函数并清理事件/订阅;滚动放在应用内部,固定内容用 `slots.header`。
33
39
  - 主题:用 `host.theme.*` 与 `--ds-*` tokens;避免硬编码颜色。
34
40
  - 宿主能力:先判断 `host.bridge.enabled`,非宿主环境要可降级运行。
@@ -85,6 +85,8 @@
85
85
 
86
86
  同时,宿主默认注入 `_meta.workdir`(默认等于 `dataDir`;如需指定其它目录,可在 `ai.mcp.callMeta.workdir` 覆盖)。
87
87
 
88
+ > DevKit sandbox:右上角 `AI Config` 支持设置 `Workdir` 覆盖;留空即使用 `dataDir`(也支持 `$dataDir/$pluginDir/$projectRoot`)。
89
+
88
90
  示例(`plugin.json`):
89
91
 
90
92
  ```json
@@ -6,7 +6,7 @@ UI Prompts 是 ChatOS 的全局交互队列:任意组件(AI / MCP / UI Apps
6
6
 
7
7
  - 存储格式:`ui-prompts.jsonl`(JSON Lines 追加日志)
8
8
  - 交互生命周期:`request` → `response`
9
- - UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm`)
9
+ - UI 渲染支持的 `prompt.kind` 与字段(`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result`)
10
10
  - UI Apps 的 Host API 调用方式(`host.uiPrompts.*`)
11
11
 
12
12
  实现对照(以代码为准):
@@ -146,7 +146,7 @@ UI Apps 的 `module` 应用通过 Host API 与 UI Prompts 交互:
146
146
 
147
147
  | 字段 | 类型 | 必填 | 说明 |
148
148
  |---|---:|---:|---|
149
- | `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` |
149
+ | `kind` | `string` | 是 | 取值:`kv` / `choice` / `task_confirm` / `file_change_confirm` / `result` |
150
150
  | `title` | `string` | 否 | UI 标题 |
151
151
  | `message` | `string` | 否 | UI 描述/说明 |
152
152
  | `source` | `string` | 否 | 来源标识(UI 显示 Tag) |
@@ -376,7 +376,36 @@ UI 渲染规则:
376
376
 
377
377
  ---
378
378
 
379
- ## 9. 复杂交互的构建方式
379
+ ## 9. `kind="result"`:执行结果(Markdown 展示)
380
+
381
+ 该类型用于“执行完成后的结果通知”。UI 会以 Markdown 形式展示结果,并在用户确认后写入 `response`。
382
+
383
+ ### 9.1 请求结构
384
+
385
+ ```json
386
+ {
387
+ "kind": "result",
388
+ "title": "执行结果",
389
+ "message": "已完成全部步骤。",
390
+ "source": "com.example.plugin:my-app",
391
+ "allowCancel": true,
392
+ "markdown": "## Done\n- step 1\n- step 2"
393
+ }
394
+ ```
395
+
396
+ 字段:
397
+
398
+ - `markdown`:可选字符串;作为 Markdown 内容展示
399
+
400
+ ### 9.2 响应结构
401
+
402
+ ```json
403
+ { "status": "ok" }
404
+ ```
405
+
406
+ ---
407
+
408
+ ## 10. 复杂交互的构建方式
380
409
 
381
410
  UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `request/response` 串联构成:
382
411
 
@@ -390,3 +419,4 @@ UI Prompts 的基本单位是一条 `request` 记录。复杂交互由多条 `re
390
419
  - “多选/单选”使用 `kind="choice"` 的 `multiple/options` 承载
391
420
  - “任务列表确认”使用 `kind="task_confirm"` 承载
392
421
  - “diff/命令确认”使用 `kind="file_change_confirm"` 承载
422
+ - “执行结果通知”使用 `kind="result"` 承载