@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 +4 -0
- package/package.json +1 -1
- package/src/mcp-followup/cursor-mcp-followup.js +79 -6
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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", () => {
|