comate-ui-inspector 0.1.1 → 0.1.2-alpha.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.
@@ -10,6 +10,7 @@
10
10
  */
11
11
  (function () {
12
12
  const alive = () => !!(chrome.runtime && chrome.runtime.id);
13
+ let lastStatusId; // 首轮只记录、不弹;之后 id 变化才弹 toast
13
14
 
14
15
  window.addEventListener('message', event => {
15
16
  if (event.source !== window || !event.data || event.data.__uiInspector !== 'selection') {
@@ -35,12 +36,22 @@
35
36
  }
36
37
  try {
37
38
  chrome.runtime.sendMessage({type: 'state'}, resp => {
38
- if (chrome.runtime.lastError) {
39
+ if (chrome.runtime.lastError || !resp || !resp.ok) {
39
40
  return;
40
41
  }
41
- if (resp && resp.ok && resp.count === 0) {
42
+ if (resp.count === 0) {
42
43
  window.postMessage({__uiInspectorBridge: 'cleared'}, '*');
43
44
  }
45
+ const status = resp.status;
46
+ if (status && status.id !== undefined) {
47
+ if (lastStatusId !== undefined && status.id !== lastStatusId) {
48
+ window.postMessage(
49
+ {__uiInspectorBridge: 'status', text: status.text, level: status.level},
50
+ '*'
51
+ );
52
+ }
53
+ lastStatusId = status.id;
54
+ }
44
55
  });
45
56
  } catch {
46
57
  clearInterval(timer);
@@ -28,6 +28,8 @@
28
28
  document.body.appendChild(hint);
29
29
 
30
30
  function showHint(text) {
31
+ window.clearTimeout(showStatus._t);
32
+ hint.style.background = '#111';
31
33
  hint.textContent = text;
32
34
  hint.style.opacity = '1';
33
35
  window.clearTimeout(showHint._t);
@@ -35,6 +37,21 @@
35
37
  hint.style.opacity = '0';
36
38
  }, 1800);
37
39
  }
40
+
41
+ // MCP notify_browser 推送的状态,复用底部这颗 pill:
42
+ // 「修改中」(info) 常驻不消失,「已完成」(success) /「失败」(error) 自动淡出。
43
+ function showStatus(text, level) {
44
+ window.clearTimeout(showHint._t);
45
+ window.clearTimeout(showStatus._t);
46
+ hint.textContent = text;
47
+ hint.style.background = level === 'success' ? '#16a34a' : level === 'error' ? '#dc2626' : '#111';
48
+ hint.style.opacity = '1';
49
+ if (level === 'success' || level === 'error') {
50
+ showStatus._t = window.setTimeout(function () {
51
+ hint.style.opacity = '0';
52
+ }, 2600);
53
+ }
54
+ }
38
55
  function cssPath(el) {
39
56
  var parts = [];
40
57
  var node = el;
@@ -162,7 +179,13 @@
162
179
  function (e) {
163
180
  if (e.key === 'Alt' && !active) {
164
181
  active = true;
165
- showHint('选择模式:点击元素可多选,再点取消;松开 Alt 退出');
182
+ showHint('选择模式:点击元素可多选,再点取消;松开 Alt 退出。点不动时按 Enter 选中当前悬停元素');
183
+ }
184
+ // 有些元素会吞掉 click(preventDefault/覆盖层),改用 Enter 选中当前 hover 的元素
185
+ if (e.key === 'Enter' && active && current && current !== overlay && current !== hint) {
186
+ e.preventDefault();
187
+ e.stopPropagation();
188
+ toggleSelect(current);
166
189
  }
167
190
  },
168
191
  true
@@ -210,14 +233,17 @@
210
233
  window.addEventListener('scroll', repositionBoxes, true);
211
234
  window.addEventListener('resize', repositionBoxes, true);
212
235
 
213
- // bridge-content 轮询到服务端 pending 已清空时回发 cleared,撤掉全部高亮
236
+ // bridge-content 轮询回发的消息:cleared(撤高亮)/ status(toast 提示)
214
237
  window.addEventListener('message', function (event) {
215
- if (event.source !== window) {
238
+ if (event.source !== window || !event.data) {
216
239
  return;
217
240
  }
218
- if (event.data && event.data.__uiInspectorBridge === 'cleared' && selected.length) {
241
+ if (event.data.__uiInspectorBridge === 'cleared' && selected.length) {
219
242
  clearSelection();
220
- showHint('已按 AI 指令取消全部选中');
243
+ showHint('已取消全部选中');
244
+ }
245
+ if (event.data.__uiInspectorBridge === 'status' && event.data.text) {
246
+ showStatus(event.data.text, event.data.level);
221
247
  }
222
248
  });
223
249
  })();
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 消息:
9
9
  * {type:'ingest', targets:[...]} → replace 写入 .ui-inspector/pending.json,回 {ok,pending}
10
- * {type:'state'} → 回 {ok,count}
10
+ * {type:'state'} → 回 {ok,count,status}(status 为最近一条 notify_browser 推送)
11
11
  *
12
12
  * 数据文件与 MCP server / hook 共用:统一在用户主目录 ~/.ui-inspector/pending.json(与仓库位置无关)。
13
13
  */
@@ -17,6 +17,7 @@ import {homedir} from 'node:os';
17
17
 
18
18
  // 共享落盘:一台机器一份,放用户主目录,不绑定任何仓库——一份代码可服务多个项目。
19
19
  const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
20
+ const statusFile = resolve(homedir(), '.ui-inspector/status.json');
20
21
 
21
22
  function readStore() {
22
23
  try {
@@ -27,6 +28,14 @@ function readStore() {
27
28
  }
28
29
  }
29
30
 
31
+ function readStatus() {
32
+ try {
33
+ return JSON.parse(readFileSync(statusFile, 'utf8'));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
30
39
  function writeStore(list) {
31
40
  mkdirSync(dirname(storeFile), {recursive: true});
32
41
  writeFileSync(storeFile, `${JSON.stringify(list, null, 4)}\n`, 'utf8');
@@ -47,7 +56,7 @@ function handle(msg) {
47
56
  writeStore(targets.map(target => ({target, receivedAt: now})));
48
57
  resp = {ok: true, pending: targets.length};
49
58
  } else if (msg && msg.type === 'state') {
50
- resp = {ok: true, count: readStore().length};
59
+ resp = {ok: true, count: readStore().length, status: readStatus()};
51
60
  } else {
52
61
  resp = {ok: false, error: 'unknown message'};
53
62
  }
@@ -5,13 +5,13 @@
5
5
  * 流程:浏览器里按住 Alt 点元素(可多选,持久高亮)→ 落盘 .ui-inspector/pending.json;
6
6
  * 回到 Comate 写修改意见提交 → 本 hook 读取**全部**选中元素,作为 additionalContext 注入给 Agent。
7
7
  *
8
- * 注意:这里**不清空** pending —— 选中态要一直保持到 AI 改完、主动调 MCP clear_pending_edits
9
- * 才取消(页面轮询到 count=0 后撤掉高亮)。否则第一轮提交就会把多选清掉、页面提前取消高亮。
8
+ * 注入后立即**清空** pending —— 「agent 拿到元素即取消选中」:页面轮询到 count=0 会撤掉高亮,
9
+ * 不必等 AI 改完。改动过程中/完成的状态由 AI 调 MCP 的 notify_browser 推给页面 toast。
10
10
  *
11
11
  * 入参由 stdin 传入(UserPromptSubmit 的 JSON),这里用不到;输出 JSON 到 stdout。
12
12
  */
13
- import {readFileSync} from 'node:fs';
14
- import {resolve} from 'node:path';
13
+ import {readFileSync, writeFileSync, mkdirSync} from 'node:fs';
14
+ import {dirname, resolve} from 'node:path';
15
15
  import {homedir} from 'node:os';
16
16
 
17
17
  // 共享落盘:读用户主目录下的 pending(与 host / mcp 同一路径),一份代码可服务多个仓库。
@@ -52,7 +52,15 @@ const body = list.map((entry, index) => describe(entry?.target ?? {}, index, tot
52
52
  const additionalContext = [
53
53
  `【ui-inspector】用户在页面上选中了 ${total} 个元素,接下来这条消息是针对它们的修改意见:`,
54
54
  body,
55
- '请据此定位源码并按用户意见修改;改完调用 MCP 工具 clear_pending_edits 取消页面选中。',
55
+ '请据此定位源码并按用户意见修改。页面选中态已自动取消;改动过程中可调用 MCP 工具 notify_browser 向页面推送状态(如「修改中…」「已完成」),会在浏览器弹 toast。',
56
56
  ].join('\n');
57
57
 
58
+ // 「拿到即取消选中」:注入后清空 pending,页面轮询到 count=0 即撤掉高亮
59
+ try {
60
+ mkdirSync(dirname(storeFile), {recursive: true});
61
+ writeFileSync(storeFile, '[]\n', 'utf8');
62
+ } catch {
63
+ // 清空失败不影响注入
64
+ }
65
+
58
66
  emit({decision: 'allow', additionalContext});
package/mcp/server.mjs CHANGED
@@ -15,6 +15,7 @@ import {homedir} from 'node:os';
15
15
  import {createInterface} from 'node:readline';
16
16
 
17
17
  const storeFile = resolve(homedir(), '.ui-inspector/pending.json');
18
+ const statusFile = resolve(homedir(), '.ui-inspector/status.json');
18
19
 
19
20
  function readStore() {
20
21
  try {
@@ -32,6 +33,14 @@ function clearStore() {
32
33
  return count;
33
34
  }
34
35
 
36
+ // 写一条状态供扩展轮询 → 页面 toast。id 单调递增(用时间戳),扩展据此判断是否是新状态。
37
+ function writeStatus(message, level) {
38
+ mkdirSync(dirname(statusFile), {recursive: true});
39
+ const status = {text: String(message ?? ''), level: level || 'info', id: Date.now()};
40
+ writeFileSync(statusFile, `${JSON.stringify(status)}\n`, 'utf8');
41
+ return status;
42
+ }
43
+
35
44
  const TOOLS = [
36
45
  {
37
46
  name: 'list_pending_edits',
@@ -45,12 +54,24 @@ const TOOLS = [
45
54
  },
46
55
  {
47
56
  name: 'clear_pending_edits',
48
- description: '处理完后清空待处理列表,返回被清空的条数。',
57
+ description: '清空待处理选中列表(取消页面高亮),返回被清空的条数。',
49
58
  inputSchema: {type: 'object', properties: {}},
50
59
  },
60
+ {
61
+ name: 'notify_browser',
62
+ description: '向浏览器页面推送一条状态提示(toast),用于告知进度,如「修改中…」「已完成」。',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ message: {type: 'string', description: '要在页面 toast 显示的文案'},
67
+ level: {type: 'string', enum: ['info', 'success', 'error'], description: '提示级别,默认 info'},
68
+ },
69
+ required: ['message'],
70
+ },
71
+ },
51
72
  ];
52
73
 
53
- function callTool(name) {
74
+ function callTool(name, args) {
54
75
  if (name === 'list_pending_edits') {
55
76
  return readStore();
56
77
  }
@@ -61,6 +82,9 @@ function callTool(name) {
61
82
  if (name === 'clear_pending_edits') {
62
83
  return {cleared: clearStore()};
63
84
  }
85
+ if (name === 'notify_browser') {
86
+ return writeStatus(args?.message, args?.level);
87
+ }
64
88
  throw new Error('unknown tool: ' + name);
65
89
  }
66
90
 
@@ -90,7 +114,7 @@ function handle(msg) {
90
114
  }
91
115
  if (msg.method === 'tools/call') {
92
116
  try {
93
- const result = callTool(msg.params?.name);
117
+ const result = callTool(msg.params?.name, msg.params?.arguments);
94
118
  send({
95
119
  jsonrpc: '2.0',
96
120
  id: msg.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comate-ui-inspector",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-alpha.1",
4
4
  "description": "按住 Alt 点选页面元素,把「元素 + Vue 组件源码路径」交给 Comate 定位源码修改。浏览器扩展 + Native Messaging host + MCP + hook。",
5
5
  "type": "module",
6
6
  "license": "MIT",