@twinsearth/w2m-dsh-plugin 2.0.1 → 2.0.2
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.0.2/RELEASE-2020.json +21 -0
- package/lib/client.js +79 -42
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/plugin/tools.mjs +1 -1
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
|
|
8
|
-
"version": "2.0.
|
|
8
|
+
"version": "2.0.2"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "w2m-fleet",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
|
|
15
|
-
"version": "2.0.
|
|
15
|
+
"version": "2.0.2",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "2.0.
|
|
29
|
+
"version": "2.0.2"
|
|
30
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w2m-fleet",
|
|
3
3
|
"description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [2.0.2] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
- 滚动按钮移到右下角,与 DSH 官方浮动滚动控件对齐(同右边缘、位于其上方;官方控件出现前后都会重新对齐)。
|
|
13
|
+
- 修复点击无反应:滚动容器改为“试滚探针”(scrollTop ±1 实际是否被浏览器接受)寻找,
|
|
14
|
+
不再依赖 overflow 样式;命中后直接写 scrollTop。
|
|
15
|
+
- 交互改为:点一次向上滚动约 3 轮;1.2 秒内连续点击 3 次回到第一轮;不再使用长按。
|
|
16
|
+
|
|
10
17
|
## [2.0.1] - 2026-10-28
|
|
11
18
|
|
|
12
19
|
- 修复右上角滚动按钮无效:滚动容器识别改为“命中测试 + 递归 shadow root + 允许 overflow:hidden 的程序化滚动容器”,
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.2",
|
|
3
|
+
"case": "RELEASE-2020",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T10:09:24.639Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-2020-1793182164640",
|
|
11
|
+
"requirement": "v2.0.2:滚动按钮移至右下与官方控件对齐;探针式滚动容器识别修复点击无效;改为点击 3 轮、连续 3 次到顶",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"position": "fixed bottom-right, aligned above the shell floating scroll control",
|
|
16
|
+
"resolver": "probe scrollTop ±1 acceptance; no overflow-style dependency",
|
|
17
|
+
"interaction": "click 1/2 scroll 3 turns; third click within 1.2s jumps to top",
|
|
18
|
+
"tests": "client-bundle 26/26, lint 0 errors"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -507,8 +507,8 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
507
507
|
'.w2mSearchInput{height:24px;min-width:220px;border:0;background:transparent;color:inherit;font:inherit;outline:none}',
|
|
508
508
|
'.w2mSearchCount{font-size:11px;opacity:.75;min-width:34px;text-align:center}',
|
|
509
509
|
'.w2mSearchControls{display:inline-flex;align-items:center;gap:4px}',
|
|
510
|
-
'.w2mScrollFloatWrap{position:fixed;right:16px;
|
|
511
|
-
'.w2mScrollFloatBtn{width:
|
|
510
|
+
'.w2mScrollFloatWrap{position:fixed;right:16px;bottom:132px;z-index:2147483000;display:flex;align-items:center;justify-content:center}',
|
|
511
|
+
'.w2mScrollFloatBtn{width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:50%;border:1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.4));background:var(--dsw-alias-background-secondary, transparent);color:inherit;opacity:.8;line-height:1;cursor:pointer;padding:0;box-shadow:0 1px 4px rgba(0,0,0,.12)}',
|
|
512
512
|
'.w2mScrollFloatBtn:hover{opacity:1}',
|
|
513
513
|
'::highlight(w2m-conversation-search){background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)}',
|
|
514
514
|
'.w2mSearchMark{background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1);border-radius:2px}',
|
|
@@ -1040,13 +1040,30 @@ function paintSearchMarks(query) {
|
|
|
1040
1040
|
}
|
|
1041
1041
|
return total;
|
|
1042
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Probe-scroll: assign a tiny scrollTop and see whether the browser keeps it.
|
|
1045
|
+
*
|
|
1046
|
+
* This is the only reliable way to find the conversation surface: overflow styles lie (a shell can
|
|
1047
|
+
* scroll an overflow:hidden element), and a virtualised list may report equal scrollHeight and
|
|
1048
|
+
* clientHeight until it is scrolled. The original scrollTop is restored immediately.
|
|
1049
|
+
*/
|
|
1043
1050
|
function isScrollableElement(el) {
|
|
1044
1051
|
if (el === null || el === undefined || el === document.body || el === document.documentElement) return false;
|
|
1045
|
-
if (typeof el.scrollHeight !== 'number' || typeof el.clientHeight !== 'number') return false;
|
|
1052
|
+
if (typeof el.scrollTop !== 'number' || typeof el.scrollHeight !== 'number' || typeof el.clientHeight !== 'number') return false;
|
|
1046
1053
|
if (el.scrollHeight <= el.clientHeight + 8) return false;
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1054
|
+
const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
|
|
1055
|
+
if (rect !== null && (rect.width < 200 || rect.height < 120)) return false;
|
|
1056
|
+
const before = el.scrollTop;
|
|
1057
|
+
try {
|
|
1058
|
+
el.scrollTop = before + 1;
|
|
1059
|
+
if (el.scrollTop !== before) { el.scrollTop = before; return true; }
|
|
1060
|
+
el.scrollTop = Math.max(0, before - 1);
|
|
1061
|
+
if (el.scrollTop !== before) { el.scrollTop = before; return true; }
|
|
1062
|
+
el.scrollTop = before;
|
|
1063
|
+
return false;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1050
1067
|
}
|
|
1051
1068
|
function findScrollSurface() {
|
|
1052
1069
|
if (typeof document === 'undefined') return null;
|
|
@@ -1055,49 +1072,42 @@ function findScrollSurface() {
|
|
|
1055
1072
|
let current = node;
|
|
1056
1073
|
while (current !== null && current !== undefined) {
|
|
1057
1074
|
if (isScrollableElement(current)) { found.add(current); return; }
|
|
1058
|
-
current = current.parentElement ?? (current.parentNode
|
|
1075
|
+
current = current.parentElement ?? (current.parentNode && current.parentNode.host ? current.parentNode.host : null);
|
|
1059
1076
|
}
|
|
1060
1077
|
};
|
|
1061
1078
|
if (typeof document.elementFromPoint === 'function' && typeof window !== 'undefined') {
|
|
1062
|
-
const points = [[window.innerWidth / 2, window.innerHeight / 2], [window.innerWidth / 2, window.innerHeight * 0.
|
|
1079
|
+
const points = [[window.innerWidth / 2, window.innerHeight / 2], [window.innerWidth / 2, window.innerHeight * 0.7], [window.innerWidth * 0.7, window.innerHeight * 0.4]];
|
|
1063
1080
|
for (const point of points) {
|
|
1064
1081
|
try { addAncestors(document.elementFromPoint(point[0], point[1])); } catch { /* ignore */ }
|
|
1065
1082
|
}
|
|
1066
1083
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
for (const el of all) if (isScrollableElement(el)) found.add(el);
|
|
1072
|
-
}
|
|
1084
|
+
for (const root of searchRoots()) {
|
|
1085
|
+
let all = [];
|
|
1086
|
+
try { all = root.querySelectorAll ? [...root.querySelectorAll('*')] : []; } catch { continue; }
|
|
1087
|
+
for (const el of all) if (isScrollableElement(el)) found.add(el);
|
|
1073
1088
|
}
|
|
1074
1089
|
let best = null;
|
|
1090
|
+
let bestArea = 0;
|
|
1075
1091
|
for (const el of found) {
|
|
1076
|
-
|
|
1092
|
+
const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
|
|
1093
|
+
const area = rect === null ? 0 : rect.width * rect.height;
|
|
1094
|
+
if (best === null || area > bestArea) { best = el; bestArea = area; }
|
|
1077
1095
|
}
|
|
1078
1096
|
return best;
|
|
1079
1097
|
}
|
|
1080
1098
|
function scrollChatTurns() {
|
|
1081
1099
|
const surface = findScrollSurface();
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (surface.scrollTop === before) surface.scrollBy({ top: -step, behavior: 'smooth' });
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
if (typeof window !== 'undefined') window.scrollBy({ top: -step, behavior: 'smooth' });
|
|
1100
|
+
const viewport = typeof window !== 'undefined' ? window.innerHeight : 800;
|
|
1101
|
+
const step = Math.round(Math.max(240, (surface !== null ? surface.clientHeight : viewport) * 1.8));
|
|
1102
|
+
if (surface !== null) surface.scrollTop = Math.max(0, surface.scrollTop - step);
|
|
1103
|
+
else if (typeof window !== 'undefined') window.scrollBy({ top: -step, behavior: 'smooth' });
|
|
1090
1104
|
}
|
|
1091
1105
|
function scrollChatTop() {
|
|
1092
1106
|
const surface = findScrollSurface();
|
|
1093
|
-
if (surface !== null)
|
|
1094
|
-
|
|
1095
|
-
surface.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1107
|
+
if (surface !== null) surface.scrollTop = 0;
|
|
1108
|
+
else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1099
1109
|
}
|
|
1100
|
-
/** One
|
|
1110
|
+
/** One circular button, aligned with the shell's own floating scroll control. */
|
|
1101
1111
|
function installFloatingScrollControl() {
|
|
1102
1112
|
if (typeof document === 'undefined' || document.body === null) return null;
|
|
1103
1113
|
const wrap = document.createElement('div');
|
|
@@ -1106,7 +1116,7 @@ function installFloatingScrollControl() {
|
|
|
1106
1116
|
const button = document.createElement('button');
|
|
1107
1117
|
button.type = 'button';
|
|
1108
1118
|
button.className = 'w2mScrollFloatBtn';
|
|
1109
|
-
button.title = '向上滚动 3
|
|
1119
|
+
button.title = '向上滚动 3 轮(连续点击 3 次回到第一轮)';
|
|
1110
1120
|
button.setAttribute('aria-label', button.title);
|
|
1111
1121
|
button.addEventListener('contextmenu', (event) => event.preventDefault());
|
|
1112
1122
|
const ns = 'http://www.w3.org/2000/svg';
|
|
@@ -1126,20 +1136,47 @@ function installFloatingScrollControl() {
|
|
|
1126
1136
|
svg.appendChild(shaft);
|
|
1127
1137
|
svg.appendChild(head);
|
|
1128
1138
|
button.appendChild(svg);
|
|
1129
|
-
let
|
|
1130
|
-
let
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
if (
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1139
|
+
let clicks = 0;
|
|
1140
|
+
let resetTimer = null;
|
|
1141
|
+
button.addEventListener('click', () => {
|
|
1142
|
+
clicks += 1;
|
|
1143
|
+
if (resetTimer !== null) clearTimeout(resetTimer);
|
|
1144
|
+
if (clicks >= 3) { clicks = 0; scrollChatTop(); return; }
|
|
1145
|
+
scrollChatTurns();
|
|
1146
|
+
resetTimer = setTimeout(() => { clicks = 0; resetTimer = null; }, 1200);
|
|
1137
1147
|
});
|
|
1138
|
-
for (const type of ['pointerup', 'pointerleave', 'pointercancel']) button.addEventListener(type, clearHold);
|
|
1139
|
-
button.addEventListener('click', () => { if (longFired) { longFired = false; return; } scrollChatTurns(); });
|
|
1140
1148
|
wrap.appendChild(button);
|
|
1141
1149
|
document.body.appendChild(wrap);
|
|
1142
|
-
|
|
1150
|
+
// Align with the shell's own floating scroll control when it is present: same right edge, just
|
|
1151
|
+
// above it. Re-run on resize because the comparison is against a live bounding rect.
|
|
1152
|
+
const align = () => {
|
|
1153
|
+
let official = null;
|
|
1154
|
+
try {
|
|
1155
|
+
const candidates = [...document.querySelectorAll('button, [role="button"]')];
|
|
1156
|
+
official = candidates.find((el) => {
|
|
1157
|
+
const label = String(el.getAttribute('aria-label') ?? el.getAttribute('title') ?? '');
|
|
1158
|
+
const rect = el.getBoundingClientRect();
|
|
1159
|
+
return /滚动到底部|滚动到顶部|回到底部|back to bottom|scroll to bottom/i.test(label) && rect.width > 0 && rect.height > 0;
|
|
1160
|
+
}) ?? null;
|
|
1161
|
+
} catch { official = null; }
|
|
1162
|
+
if (official === null) {
|
|
1163
|
+
wrap.style.right = '16px';
|
|
1164
|
+
wrap.style.bottom = '132px';
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const rect = official.getBoundingClientRect();
|
|
1168
|
+
wrap.style.right = Math.max(8, Math.round(window.innerWidth - rect.right)) + 'px';
|
|
1169
|
+
wrap.style.bottom = Math.max(8, Math.round(window.innerHeight - rect.top + 8)) + 'px';
|
|
1170
|
+
};
|
|
1171
|
+
align();
|
|
1172
|
+
for (const delay of [120, 600, 1500]) setTimeout(align, delay);
|
|
1173
|
+
const onResize = () => align();
|
|
1174
|
+
if (typeof window !== 'undefined') window.addEventListener('resize', onResize);
|
|
1175
|
+
return () => {
|
|
1176
|
+
if (resetTimer !== null) clearTimeout(resetTimer);
|
|
1177
|
+
if (typeof window !== 'undefined') window.removeEventListener('resize', onResize);
|
|
1178
|
+
wrap.remove();
|
|
1179
|
+
};
|
|
1143
1180
|
}
|
|
1144
1181
|
function ConversationSearch() {
|
|
1145
1182
|
const [open, setOpen] = React.useState(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -286,7 +286,7 @@ const MAX_STAGES = 16;
|
|
|
286
286
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
287
287
|
* overrides it at runtime for tests and packagers.
|
|
288
288
|
*/
|
|
289
|
-
const PLUGIN_VERSION = '2.0.
|
|
289
|
+
const PLUGIN_VERSION = '2.0.2';
|
|
290
290
|
|
|
291
291
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
292
292
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|