@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.
@@ -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.1"
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.1",
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.1"
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.1",
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 的程序化滚动容器”,
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@2.0.1", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@2.0.2", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -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;top:64px;z-index:2147483000;display:flex;flex-direction:column;align-items:center}',
511
- '.w2mScrollFloatBtn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;border:1px solid rgba(128,128,128,.4);background:transparent;color:inherit;opacity:.72;line-height:1;cursor:pointer;padding:0}',
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 style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
1048
- const overflow = style === null ? '' : style.overflowY;
1049
- return overflow === '' || overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay' || overflow === 'hidden';
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 instanceof ShadowRoot ? current.parentNode.host : null);
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.72], [window.innerWidth * 0.72, window.innerHeight * 0.4]];
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
- if (found.size === 0) {
1068
- for (const root of searchRoots()) {
1069
- let all = [];
1070
- try { all = root.querySelectorAll ? [...root.querySelectorAll('*')] : []; } catch { continue; }
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
- if (best === null || el.clientHeight * el.scrollHeight > best.clientHeight * best.scrollHeight) best = el;
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 step = Math.round(Math.max(240, (surface !== null ? surface.clientHeight : window.innerHeight) * 1.8));
1083
- if (surface !== null) {
1084
- const before = surface.scrollTop;
1085
- surface.scrollTop = before - step;
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
- surface.scrollTop = 0;
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 floating button, styled after the shell's own back-to-top control. */
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 轮(长按 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 holdTimer = null;
1130
- let longFired = false;
1131
- const clearHold = () => { if (holdTimer !== null) { clearTimeout(holdTimer); holdTimer = null; } };
1132
- button.addEventListener('pointerdown', (event) => {
1133
- if (event && typeof event.preventDefault === 'function') event.preventDefault();
1134
- longFired = false;
1135
- clearHold();
1136
- holdTimer = setTimeout(() => { longFired = true; scrollChatTop(); }, 3000);
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
- return () => { clearHold(); wrap.remove(); };
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.1",
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.1",
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": [
@@ -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.1';
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;