@leeoohoo/ui-apps-devkit 0.1.7 → 0.1.9

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.7",
3
+ "version": "0.1.9",
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",
@@ -8,6 +8,8 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
8
8
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
9
9
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
10
10
  import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
11
+ import { LoggingMessageNotificationSchema, NotificationSchema } from '@modelcontextprotocol/sdk/types.js';
12
+ import * as z from 'zod/v4';
11
13
 
12
14
  import { copyDir, ensureDir, isDirectory, isFile } from '../lib/fs.js';
13
15
  import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
@@ -273,6 +275,34 @@ function formatMcpToolResult(serverName, toolName, result) {
273
275
  return `${header}\n${segments.join('\n\n')}`;
274
276
  }
275
277
 
278
+ const MCP_STREAM_NOTIFICATION_METHODS = [
279
+ 'codex_app.window_run.stream',
280
+ 'codex_app.window_run.done',
281
+ 'codex_app.window_run.completed',
282
+ ];
283
+
284
+ const buildLooseNotificationSchema = (method) =>
285
+ NotificationSchema.extend({
286
+ method: z.literal(method),
287
+ params: z.unknown().optional(),
288
+ });
289
+
290
+ function registerMcpNotificationHandlers(client, { serverName, onNotification } = {}) {
291
+ if (!client || typeof client.setNotificationHandler !== 'function') return;
292
+ if (typeof onNotification !== 'function') return;
293
+ const emit = (notification) => {
294
+ try {
295
+ onNotification({ serverName, ...notification });
296
+ } catch {
297
+ // ignore notification relay errors
298
+ }
299
+ };
300
+ client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => emit(notification));
301
+ MCP_STREAM_NOTIFICATION_METHODS.forEach((method) => {
302
+ client.setNotificationHandler(buildLooseNotificationSchema(method), (notification) => emit(notification));
303
+ });
304
+ }
305
+
276
306
  async function listAllMcpTools(client) {
277
307
  const collected = [];
278
308
  let cursor = null;
@@ -290,15 +320,17 @@ async function listAllMcpTools(client) {
290
320
  return collected;
291
321
  }
292
322
 
293
- async function connectMcpServer(entry) {
323
+ async function connectMcpServer(entry, options = {}) {
294
324
  if (!entry || typeof entry !== 'object') return null;
295
325
  const serverName = normalizeText(entry.name) || 'mcp_server';
326
+ const onNotification = typeof options?.onNotification === 'function' ? options.onNotification : null;
296
327
  const env = { ...process.env };
297
328
  if (!env.MODEL_CLI_SESSION_ROOT) env.MODEL_CLI_SESSION_ROOT = process.cwd();
298
329
  if (!env.MODEL_CLI_WORKSPACE_ROOT) env.MODEL_CLI_WORKSPACE_ROOT = process.cwd();
299
330
 
300
331
  if (entry.command) {
301
332
  const client = new Client({ name: 'sandbox', version: '0.1.0' });
333
+ registerMcpNotificationHandlers(client, { serverName, onNotification });
302
334
  const transport = new StdioClientTransport({
303
335
  command: entry.command,
304
336
  args: Array.isArray(entry.args) ? entry.args : [],
@@ -318,6 +350,7 @@ async function connectMcpServer(entry) {
318
350
  if (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') {
319
351
  const client = new Client({ name: 'sandbox', version: '0.1.0' });
320
352
  const transport = new WebSocketClientTransport(parsed);
353
+ registerMcpNotificationHandlers(client, { serverName, onNotification });
321
354
  await client.connect(transport);
322
355
  const tools = await listAllMcpTools(client);
323
356
  return { serverName, client, transport, tools };
@@ -327,6 +360,7 @@ async function connectMcpServer(entry) {
327
360
  try {
328
361
  const client = new Client({ name: 'sandbox', version: '0.1.0' });
329
362
  const transport = new StreamableHTTPClientTransport(parsed);
363
+ registerMcpNotificationHandlers(client, { serverName, onNotification });
330
364
  await client.connect(transport);
331
365
  const tools = await listAllMcpTools(client);
332
366
  return { serverName, client, transport, tools };
@@ -336,6 +370,7 @@ async function connectMcpServer(entry) {
336
370
  try {
337
371
  const client = new Client({ name: 'sandbox', version: '0.1.0' });
338
372
  const transport = new SSEClientTransport(parsed);
373
+ registerMcpNotificationHandlers(client, { serverName, onNotification });
339
374
  await client.connect(transport);
340
375
  const tools = await listAllMcpTools(client);
341
376
  return { serverName, client, transport, tools };
@@ -1407,6 +1442,7 @@ function renderPrompts() {
1407
1442
  form.style.gap = '10px';
1408
1443
 
1409
1444
  const kind = String(req?.prompt?.kind || '');
1445
+ const allowCancel = req?.prompt?.allowCancel !== false;
1410
1446
 
1411
1447
  const mkBtn = (label, danger) => {
1412
1448
  const btn = document.createElement('button');
@@ -1421,7 +1457,31 @@ function renderPrompts() {
1421
1457
  emitUpdate();
1422
1458
  };
1423
1459
 
1424
- if (kind === 'kv') {
1460
+ if (kind === 'result') {
1461
+ const markdownText =
1462
+ typeof req?.prompt?.markdown === 'string'
1463
+ ? req.prompt.markdown
1464
+ : typeof req?.prompt?.result === 'string'
1465
+ ? req.prompt.result
1466
+ : typeof req?.prompt?.content === 'string'
1467
+ ? req.prompt.content
1468
+ : '';
1469
+ const markdown = document.createElement('pre');
1470
+ markdown.className = 'mono';
1471
+ markdown.textContent = markdownText || '(无结果内容)';
1472
+ form.appendChild(markdown);
1473
+ const row = document.createElement('div');
1474
+ row.className = 'row';
1475
+ const ok = mkBtn('OK');
1476
+ ok.addEventListener('click', () => submit({ status: 'ok' }));
1477
+ row.appendChild(ok);
1478
+ if (allowCancel) {
1479
+ const cancel = mkBtn('Cancel', true);
1480
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1481
+ row.appendChild(cancel);
1482
+ }
1483
+ form.appendChild(row);
1484
+ } else if (kind === 'kv') {
1425
1485
  const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
1426
1486
  const values = {};
1427
1487
  for (const f of fields) {
@@ -1443,10 +1503,12 @@ function renderPrompts() {
1443
1503
  row.className = 'row';
1444
1504
  const ok = mkBtn('Submit');
1445
1505
  ok.addEventListener('click', () => submit({ status: 'ok', values }));
1446
- const cancel = mkBtn('Cancel', true);
1447
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1448
1506
  row.appendChild(ok);
1449
- row.appendChild(cancel);
1507
+ if (allowCancel) {
1508
+ const cancel = mkBtn('Cancel', true);
1509
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1510
+ row.appendChild(cancel);
1511
+ }
1450
1512
  form.appendChild(row);
1451
1513
  } else if (kind === 'choice') {
1452
1514
  const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
@@ -1475,20 +1537,24 @@ function renderPrompts() {
1475
1537
  row.className = 'row';
1476
1538
  const ok = mkBtn('Submit');
1477
1539
  ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
1478
- const cancel = mkBtn('Cancel', true);
1479
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1480
1540
  row.appendChild(ok);
1481
- row.appendChild(cancel);
1541
+ if (allowCancel) {
1542
+ const cancel = mkBtn('Cancel', true);
1543
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1544
+ row.appendChild(cancel);
1545
+ }
1482
1546
  form.appendChild(row);
1483
1547
  } else {
1484
1548
  const row = document.createElement('div');
1485
1549
  row.className = 'row';
1486
1550
  const ok = mkBtn('OK');
1487
1551
  ok.addEventListener('click', () => submit({ status: 'ok' }));
1488
- const cancel = mkBtn('Cancel', true);
1489
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1490
1552
  row.appendChild(ok);
1491
- row.appendChild(cancel);
1553
+ if (allowCancel) {
1554
+ const cancel = mkBtn('Cancel', true);
1555
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1556
+ row.appendChild(cancel);
1557
+ }
1492
1558
  form.appendChild(row);
1493
1559
  }
1494
1560
 
@@ -1887,6 +1953,19 @@ const scheduleReload = (() => {
1887
1953
  try {
1888
1954
  const es = new EventSource('/events');
1889
1955
  es.addEventListener('reload', () => scheduleReload());
1956
+ es.addEventListener('mcp-notification', (event) => {
1957
+ if (!event?.data) return;
1958
+ let payload = null;
1959
+ try {
1960
+ payload = JSON.parse(event.data);
1961
+ } catch {
1962
+ payload = { raw: event.data };
1963
+ }
1964
+ if (!payload) return;
1965
+ const server = payload?.serverName ? String(payload.serverName) : 'mcp';
1966
+ const method = payload?.method ? String(payload.method) : 'notification';
1967
+ appendMcpOutput(`${server} ${method}`, payload?.params?.text || payload);
1968
+ });
1890
1969
  } catch {
1891
1970
  // ignore
1892
1971
  }
@@ -1939,6 +2018,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1939
2018
  let mcpRuntime = null;
1940
2019
  let mcpRuntimePromise = null;
1941
2020
  let sandboxCallMeta = null;
2021
+ let relayMcpNotification = null;
1942
2022
 
1943
2023
  const resetMcpRuntime = async () => {
1944
2024
  const runtime = mcpRuntime;
@@ -1965,7 +2045,9 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1965
2045
  if (mcpRuntime) return mcpRuntime;
1966
2046
  if (!mcpRuntimePromise) {
1967
2047
  mcpRuntimePromise = (async () => {
1968
- const handle = await connectMcpServer(appMcpEntry);
2048
+ const handle = await connectMcpServer(appMcpEntry, {
2049
+ onNotification: relayMcpNotification,
2050
+ });
1969
2051
  if (!handle) return null;
1970
2052
  const toolEntries = Array.isArray(handle.tools)
1971
2053
  ? handle.tools.map((tool) => {
@@ -2172,6 +2254,13 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
2172
2254
  sseWrite(res, event, data);
2173
2255
  }
2174
2256
  };
2257
+ relayMcpNotification = (notification) => {
2258
+ if (!notification) return;
2259
+ sseBroadcast('mcp-notification', {
2260
+ ...notification,
2261
+ receivedAt: new Date().toISOString(),
2262
+ });
2263
+ };
2175
2264
 
2176
2265
  let changeSeq = 0;
2177
2266
  const stopWatch = startRecursiveWatcher(pluginDir, ({ eventType, filePath }) => {
@@ -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"` 承载
@@ -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"` 承载