comate-ui-inspector 0.1.0 → 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.
package/cli.mjs CHANGED
@@ -34,7 +34,11 @@ function nmDirs() {
34
34
  'Library/Application Support/Microsoft Edge/NativeMessagingHosts',
35
35
  'Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts',
36
36
  ]
37
- : ['.config/google-chrome/NativeMessagingHosts', '.config/microsoft-edge/NativeMessagingHosts', '.config/chromium/NativeMessagingHosts'];
37
+ : [
38
+ '.config/google-chrome/NativeMessagingHosts',
39
+ '.config/microsoft-edge/NativeMessagingHosts',
40
+ '.config/chromium/NativeMessagingHosts',
41
+ ];
38
42
  return rels.map(rel => join(home, rel));
39
43
  }
40
44
 
@@ -51,10 +55,15 @@ function out(text) {
51
55
  }
52
56
  function install() {
53
57
  chmodSync(hostPath, 0o755);
58
+ // 用壳脚本硬编码 node 绝对路径:Chrome 拉起 native host 时 PATH 极简(不含 nvm 等目录),
59
+ // 直接 `#!/usr/bin/env node` 会找不到 node 导致 host 起不来;壳脚本规避这点。
60
+ const wrapperPath = resolve(pkgRoot, 'extension/native-host/host-wrapper.sh');
61
+ writeFileSync(wrapperPath, `#!/bin/sh\nexec "${process.execPath}" "${hostPath}" "$@"\n`);
62
+ chmodSync(wrapperPath, 0o755);
54
63
  const manifest = {
55
64
  name: HOST_NAME,
56
65
  description: 'ui-inspector native messaging host',
57
- path: hostPath,
66
+ path: wrapperPath,
58
67
  type: 'stdio',
59
68
  allowed_origins: [`chrome-extension://${EXT_ID}/`],
60
69
  };
@@ -97,6 +106,10 @@ function uninstall({purge}) {
97
106
  out(`removed host manifest -> ${file}`);
98
107
  }
99
108
  }
109
+ const wrapperPath = resolve(pkgRoot, 'extension/native-host/host-wrapper.sh');
110
+ if (existsSync(wrapperPath)) {
111
+ rmSync(wrapperPath);
112
+ }
100
113
  const config = readHooks();
101
114
  if (config?.hooks?.UserPromptSubmit) {
102
115
  const before = config.hooks.UserPromptSubmit.length;
@@ -151,4 +164,3 @@ if (cmd === 'install') {
151
164
  } else {
152
165
  help();
153
166
  }
154
-
@@ -1,29 +1,60 @@
1
1
  /*
2
2
  * ISOLATED world 内容脚本:MAIN world 与 background 之间的中继。
3
3
  *
4
- * - 收到 MAIN 发来的 selection(window.postMessage)→ 转给 background 写入本地桥;
5
- * - 每 1.5s 问 background 要一次 /state 的 count;count 为 0(AI 调 MCP 清空后)
4
+ * - 收到 MAIN 发来的 selection(window.postMessage)→ 转给 background 写入本地 host;
5
+ * - 每 1.5s 问 background 要一次 state 的 count;count 为 0(AI 调 MCP 清空后)
6
6
  * → 回发 cleared 给 MAIN,让它撤掉页面高亮。
7
+ *
8
+ * 健壮性:扩展被重载后旧页面里的本脚本会「上下文失效」,此时访问 chrome.runtime 会抛
9
+ * "Extension context invalidated"。用 alive() 判活 + try/catch 兜住,并在失效时停掉轮询。
7
10
  */
8
11
  (function () {
12
+ const alive = () => !!(chrome.runtime && chrome.runtime.id);
13
+ let lastStatusId; // 首轮只记录、不弹;之后 id 变化才弹 toast
14
+
9
15
  window.addEventListener('message', event => {
10
- if (event.source !== window) {
16
+ if (event.source !== window || !event.data || event.data.__uiInspector !== 'selection') {
17
+ return;
18
+ }
19
+ if (!alive()) {
11
20
  return;
12
21
  }
13
- const data = event.data;
14
- if (data && data.__uiInspector === 'selection') {
15
- chrome.runtime.sendMessage({type: 'ingest', targets: data.targets || []});
22
+ try {
23
+ chrome.runtime.sendMessage(
24
+ {type: 'ingest', targets: event.data.targets || []},
25
+ () => void chrome.runtime.lastError
26
+ );
27
+ } catch {
28
+ // 扩展已重载、上下文失效:刷新页面即恢复
16
29
  }
17
30
  });
18
31
 
19
- setInterval(() => {
20
- chrome.runtime.sendMessage({type: 'state'}, resp => {
21
- if (chrome.runtime.lastError) {
22
- return;
23
- }
24
- if (resp && resp.ok && resp.count === 0) {
25
- window.postMessage({__uiInspectorBridge: 'cleared'}, '*');
26
- }
27
- });
32
+ const timer = setInterval(() => {
33
+ if (!alive()) {
34
+ clearInterval(timer);
35
+ return;
36
+ }
37
+ try {
38
+ chrome.runtime.sendMessage({type: 'state'}, resp => {
39
+ if (chrome.runtime.lastError || !resp || !resp.ok) {
40
+ return;
41
+ }
42
+ if (resp.count === 0) {
43
+ window.postMessage({__uiInspectorBridge: 'cleared'}, '*');
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
+ }
55
+ });
56
+ } catch {
57
+ clearInterval(timer);
58
+ }
28
59
  }, 1500);
29
60
  })();
@@ -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.0",
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",