@srgay/cursor-extension 1.0.5 → 1.0.7

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/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
  2. **MCP Follow-up 面板**:在 Cursor 聊天框上方注入一个 MCP 反馈面板,连接 `mcp-feedback-enhanced` 的 WebSocket,让你直接在 Cursor 里回复 `interactive_feedback`,支持端口自动扫描/自定义,并能按当前窗口自动匹配对应端口(多窗口、多端口同时在线时也不误连)。
7
7
  3. **输入法回车修复**:修复用中文输入法(拼音/注音等)组字时,按回车上屏候选词会被 Cursor 误当成「提交」的问题(含自带的 AskQuestion「Other」输入框)。
8
8
 
9
+ ## 致谢
10
+
11
+ 感谢 [LINUX DO](https://linux.do) 社区的交流氛围与灵感支持。
12
+
9
13
  ## 安装与使用(npx)
10
14
 
11
15
  无需克隆仓库,直接用 `npx` 运行(需 Node.js ≥ 20):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srgay/cursor-extension",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "本机 Cursor workbench 增强:MAX Mode 守护、MCP Follow-up 面板、输入法回车修复(支持随 Cursor 启动持久加载,或 CDP 临时注入)。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,9 @@
27
27
  pyCmd: "__MCP_PY__",
28
28
  // run_command 拉取提示词的兜底超时(毫秒)。
29
29
  promptLoadTimeoutMs: 4000,
30
+ // onclose 断开后「自动重扫一次」的最小间隔(毫秒)。仅用于事件驱动的防抖,
31
+ // 防止「连不上→断开→再扫」抖动成风暴;不是后台定时轮询。
32
+ autoRescanMinGapMs: 4000,
30
33
  };
31
34
 
32
35
  const state = {
@@ -69,6 +72,10 @@
69
72
  // 自定义端口下拉的文档级监听(点空白处 / Esc 关闭菜单),uninstall 时移除。
70
73
  onDocPointerDown: null,
71
74
  onDocKeydown: null,
75
+ // 上次扫描的时间戳,供 onclose 断开后的「防抖自动重扫」判断间隔(事件驱动,非定时)。
76
+ lastAutoScanAt: 0,
77
+ // 正在对某端口做「守卫拒绝前的归属复核」探测,避免对同一端口重复探测。
78
+ revalidating: null,
72
79
  };
73
80
 
74
81
  const SVG_NS = "http://www.w3.org/2000/svg";
@@ -741,6 +748,23 @@
741
748
  return hit ? String(hit.port) : null;
742
749
  }
743
750
 
751
+ // 端口对应窗口的「真实工作区名」:取自 Cursor 注入到该端口 MCP 服务进程的 WORKSPACE_FOLDER_PATHS
752
+ // (多根工作区取第一个),次选 CURSOR_WORKSPACE_LABEL。两者均来自 env、不受 AI 传入的
753
+ // project_directory 影响,是判断「端口属于哪个窗口/项目」最可信的来源;取不到时返回空串。
754
+ function portWorkspaceName(port) {
755
+ const key = String(port);
756
+ const folders = state.portWorkspaces[key];
757
+ if (folders) {
758
+ const first = String(folders)
759
+ .split(/[:;,\n]/)
760
+ .map((s) => s.trim())
761
+ .filter(Boolean)[0];
762
+ const name = basename(first);
763
+ if (name) return name;
764
+ }
765
+ return state.portLabels[key] || "";
766
+ }
767
+
744
768
  // 选项文本:Port {port}[ · 状态][ · 项目名](状态在前、项目在后)。
745
769
  // 下拉展开时所有项都带「状态 · 项目名」,便于按项目/状态区分端口;
746
770
  // 收起时框里显示的是「选中项」,为避免与右侧项目名重复,选中项收起时不带项目名。
@@ -748,7 +772,8 @@
748
772
  let text = "Port " + port;
749
773
  if (status) text += " · " + status;
750
774
  if (withProject) {
751
- const project = state.portProjects[String(port)];
775
+ // 项目名优先用 env 的真实工作区名(WORKSPACE_FOLDER_PATHS),取不到才回退 AI 会话的 project_directory。
776
+ const project = portWorkspaceName(port) || state.portProjects[String(port)];
752
777
  if (project) text += " · " + project;
753
778
  }
754
779
  return text;
@@ -867,14 +892,17 @@
867
892
  }
868
893
 
869
894
  function sessionTitle(data) {
870
- const name = basename(data && data.project_directory);
895
+ // env 的真实工作区名最可信;取不到才回退 AI 会话的 project_directory
896
+ const name = portWorkspaceName(state.socketPort) || basename(data && data.project_directory);
871
897
  return name ? " · " + name : "";
872
898
  }
873
899
 
874
900
  function updateProjectName(data) {
901
+ // 头部项目名同样以 env 的真实工作区为主、AI 会话 project_directory 兜底(连不上 env 时仍有显示)。
902
+ const envName = portWorkspaceName(state.socketPort);
875
903
  const dir = data && data.project_directory ? data.project_directory : "";
876
- els.projectName.textContent = basename(dir) || "No project";
877
- els.projectName.title = dir || "";
904
+ els.projectName.textContent = envName || basename(dir) || "No project";
905
+ els.projectName.title = state.portWorkspaces[String(state.socketPort)] || dir || "";
878
906
  }
879
907
 
880
908
  function setVisualState(stateName, message) {
@@ -1523,6 +1551,7 @@
1523
1551
  const refreshOnly = !!(opts && opts.refreshOnly);
1524
1552
  if (state.scanning) return;
1525
1553
  state.scanning = true;
1554
+ state.lastAutoScanAt = Date.now();
1526
1555
  if (els.scanBtn) {
1527
1556
  els.scanBtn.classList.add("scanning");
1528
1557
  els.scanBtn.disabled = true;
@@ -1647,12 +1676,41 @@
1647
1676
  }
1648
1677
  }
1649
1678
 
1679
+ // ③ 断开后「自动重扫一次」:事件驱动 + 防抖(非定时轮询)。借扫描重新探测各端口归属、
1680
+ // 刷新缓存并连回本窗口端口;防抖避免「连不上→断开→再扫」反复抖动成风暴。
1681
+ function autoRescan() {
1682
+ if (state.scanning) return;
1683
+ if (els.portSelect.value === config.customValue) return;
1684
+ if (Date.now() - state.lastAutoScanAt < config.autoRescanMinGapMs) return;
1685
+ scanPorts();
1686
+ }
1687
+
1688
+ // ④ 守卫判定「别的窗口」后,不全信可能过时的缓存:对该端口做一次性探测复核真实归属,
1689
+ // 刷新缓存;仅当探测确认「确属本窗口」时才重连,否则保持离线等待(不轮询)。
1690
+ function revalidatePortOwnership(port, ws) {
1691
+ const key = String(port);
1692
+ if (state.revalidating === key) return;
1693
+ state.revalidating = key;
1694
+ probePort(key, config.probeTimeoutMs).then((res) => {
1695
+ if (state.revalidating === key) state.revalidating = null;
1696
+ if (res.workspaceFolders) state.portWorkspaces[key] = res.workspaceFolders;
1697
+ else delete state.portWorkspaces[key];
1698
+ if (res.workspaceLabel) state.portLabels[key] = res.workspaceLabel;
1699
+ else delete state.portLabels[key];
1700
+ // 用户没切走端口、探测存活且复核后确属本窗口,才连接(连接层会再次走守卫兜底)。
1701
+ if (els.portSelect.value === key && res.alive && isPortMine(key, ws)) {
1702
+ connectSelectedPort(false);
1703
+ }
1704
+ });
1705
+ }
1706
+
1650
1707
  function connectSelectedPort(auto) {
1651
1708
  const port = els.portSelect.value;
1652
1709
  if (port === config.customValue) return;
1653
1710
 
1654
1711
  // 守卫:能确定当前窗口、且该端口已知属于别的窗口时不连接,
1655
- // 避免误连到其它窗口的实例或常驻 feedback 实例(等本窗口端口被识别后由 refreshTick 重扫接管)。
1712
+ // 避免误连到其它窗口的实例或常驻 feedback 实例(随后由 revalidatePortOwnership 探测复核,
1713
+ // 若该端口已换回本窗口则立刻重连接管)。
1656
1714
  const ws = getWorkspacePath();
1657
1715
  const known = !!(state.portWorkspaces[port] || state.portLabels[port]);
1658
1716
  if (ws && known && !isPortMine(port, ws)) {
@@ -1665,6 +1723,9 @@
1665
1723
  }
1666
1724
  setOptionLabel(port, "other window");
1667
1725
  setVisualState("offline", port + " 属于其它窗口,等待本窗口的 MCP 会话…");
1726
+ // ④ 缓存可能已过时(如该端口刚换回本窗口):异步探测复核,确属本窗口则立刻重连,
1727
+ // 避免「端口已是自己的、却被 stale 缓存永久判为别人的」而连不上。
1728
+ revalidatePortOwnership(port, ws);
1668
1729
  return;
1669
1730
  }
1670
1731
 
@@ -1734,6 +1795,12 @@
1734
1795
  if (seq !== state.connectSeq) return;
1735
1796
  state.socket = null;
1736
1797
  if (state.currentState === "processing") return;
1798
+ // ① 非 4004 的断开意味着端口可能已关闭/换主:清除该端口归属缓存,从根上消除 stale,
1799
+ // 避免下次连接被过时归属(守卫)误判。4004 是「连上了但无会话」,端口归属仍有效,不清。
1800
+ if (event.code !== 4004) {
1801
+ delete state.portWorkspaces[port];
1802
+ delete state.portLabels[port];
1803
+ }
1737
1804
  setOptionLabel(port, event.code === 4004 ? "no session" : "offline");
1738
1805
  setVisualState(
1739
1806
  "offline",
@@ -1741,6 +1808,8 @@
1741
1808
  ? port + " 已连接到 MCP,但当前没有 active session。"
1742
1809
  : port + " WebSocket 已断开。"
1743
1810
  );
1811
+ // ③ 断开后自动重扫一次(事件驱动 + 防抖):刷新各端口归属并连回本窗口端口;不聚焦也能自愈。
1812
+ if (event.code !== 4004) autoRescan();
1744
1813
  };
1745
1814
  } catch (error) {
1746
1815
  setOptionLabel(port, "offline");
@@ -1908,7 +1977,11 @@
1908
1977
  suppressAutoSubmit();
1909
1978
  });
1910
1979
  els.prompt.addEventListener("focus", () => {
1911
- if (els.portSelect.value !== config.customValue) connectSelectedPort(true);
1980
+ if (els.portSelect.value === config.customValue) return;
1981
+ // ② 离线时聚焦:缓存可能过时导致守卫挡住直连,改为重扫一次刷新各端口归属并连回本窗口端口
1982
+ // (事件驱动 + 防抖);非离线仍走原「重连抢回最后连接」逻辑。
1983
+ if (state.currentState === "offline") autoRescan();
1984
+ else connectSelectedPort(true);
1912
1985
  });
1913
1986
  // 跟踪输入法组字状态:组字期间(含上屏候选词的回车)不触发发送。
1914
1987
  els.prompt.addEventListener("compositionstart", () => {