@twinsearth/w2m-dsh-plugin 2.0.1 → 2.0.3

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.3"
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.3",
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.3"
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.3",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [2.0.3] - 2026-10-28
11
+
12
+ - 滚动按钮移到会话正文列右上角(黑框位置):按官方浮动圆钮规格 34px、无边框、elevation 阴影、
13
+ label-primary 图标色(亮色显示为黑色,暗色自动反白)、hover/active/focus-visible 状态。
14
+ - 位置由会话滚动区实时计算(正文列右缘 + 顶部内缩 8px),窗口缩放与侧栏变化时重新对齐。
15
+ - 关键修复:自动完成 DSH 官方“加载更早”——会话是虚拟列表,不点“加载更早”时 scrollTop 到顶即止,
16
+ 这是之前“点击无效/无法到顶”的真正原因。现在滚动前若已在顶部会先点一次“加载更早”等待 350ms,
17
+ “连续点击 3 次到顶”会循环点击“加载更早”直到它消失,再把滚动区钉在顶部。
18
+ - 交互:点一次向上滚动约 3 轮;1.2 秒内连续点击 3 次回到第一轮。
19
+
20
+ ## [2.0.2] - 2026-10-28
21
+
22
+ - 滚动按钮移到右下角,与 DSH 官方浮动滚动控件对齐(同右边缘、位于其上方;官方控件出现前后都会重新对齐)。
23
+ - 修复点击无反应:滚动容器改为“试滚探针”(scrollTop ±1 实际是否被浏览器接受)寻找,
24
+ 不再依赖 overflow 样式;命中后直接写 scrollTop。
25
+ - 交互改为:点一次向上滚动约 3 轮;1.2 秒内连续点击 3 次回到第一轮;不再使用长按。
26
+
10
27
  ## [2.0.1] - 2026-10-28
11
28
 
12
29
  - 修复右上角滚动按钮无效:滚动容器识别改为“命中测试 + 递归 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.3", "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
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "2.0.3",
3
+ "case": "RELEASE-2030",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-28T10:35:33.753Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-2030-1793183733754",
11
+ "requirement": "v2.0.3:滚动按钮移到会话区右上黑框位置并变黑;自动点击“加载更早”解除虚拟列表阻断;点击 3 轮、连续 3 次到顶",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "position": "conversation column top-right, 8px inset from surface top, right edge aligned",
16
+ "style": "34px circle, no border, elevation shadow, label-primary icon via theme tokens",
17
+ "load_earlier": "auto-click before scrolling at top and repeatedly during scroll-to-top",
18
+ "interaction": "click 3 turns; 3 clicks within 1.2s to top",
19
+ "tests": "client-bundle 26/26, lint 0 errors"
20
+ }
21
+ }
22
+ }
package/lib/client.js CHANGED
@@ -507,9 +507,12 @@ 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}',
512
- '.w2mScrollFloatBtn:hover{opacity:1}',
510
+ '.w2mScrollFloatWrap{position:fixed;top:120px;right:16px;bottom:auto;left:auto;z-index:20;display:flex;align-items:center;justify-content:center;pointer-events:none}',
511
+ '.w2mScrollFloatBtn{box-sizing:border-box;width:34px;height:34px;padding:0;border:0;border-radius:100px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-button-floating-fill);box-shadow:0 0 0 .5px var(--dsw-alias-border-l3,rgba(0,0,0,.12)),0 3px 8px 0 rgba(0,0,0,.031),0 0 16px 0 rgba(0,0,0,.02);opacity:1;line-height:1;cursor:pointer;pointer-events:auto;transition:background-color 150ms ease,transform 150ms ease}',
512
+ '.w2mScrollFloatBtn:hover{background:var(--dsw-alias-button-floating-hover)}',
513
+ '.w2mScrollFloatBtn:active{background:var(--dsw-alias-button-ghost-active-fill);transform:scale(.96)}',
514
+ '.w2mScrollFloatBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}',
515
+ '.w2mScrollFloatBtn svg{display:block;width:16px;height:16px;stroke-width:1.5}',
513
516
  '::highlight(w2m-conversation-search){background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)}',
514
517
  '.w2mSearchMark{background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1);border-radius:2px}',
515
518
  '.w2mSearchBtn{height:24px;min-width:24px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
@@ -1040,13 +1043,30 @@ function paintSearchMarks(query) {
1040
1043
  }
1041
1044
  return total;
1042
1045
  }
1046
+ /**
1047
+ * Probe-scroll: assign a tiny scrollTop and see whether the browser keeps it.
1048
+ *
1049
+ * Overflow styles lie (a shell can scroll an overflow:hidden element), and a virtualised list may
1050
+ * report equal scrollHeight and clientHeight until it is scrolled. The original scrollTop is
1051
+ * restored immediately.
1052
+ */
1043
1053
  function isScrollableElement(el) {
1044
1054
  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;
1055
+ if (typeof el.scrollTop !== 'number' || typeof el.scrollHeight !== 'number' || typeof el.clientHeight !== 'number') return false;
1046
1056
  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';
1057
+ const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
1058
+ if (rect !== null && (rect.width < 200 || rect.height < 120)) return false;
1059
+ const before = el.scrollTop;
1060
+ try {
1061
+ el.scrollTop = before + 1;
1062
+ if (el.scrollTop !== before) { el.scrollTop = before; return true; }
1063
+ el.scrollTop = Math.max(0, before - 1);
1064
+ if (el.scrollTop !== before) { el.scrollTop = before; return true; }
1065
+ el.scrollTop = before;
1066
+ return false;
1067
+ } catch {
1068
+ return false;
1069
+ }
1050
1070
  }
1051
1071
  function findScrollSurface() {
1052
1072
  if (typeof document === 'undefined') return null;
@@ -1055,49 +1075,81 @@ function findScrollSurface() {
1055
1075
  let current = node;
1056
1076
  while (current !== null && current !== undefined) {
1057
1077
  if (isScrollableElement(current)) { found.add(current); return; }
1058
- current = current.parentElement ?? (current.parentNode instanceof ShadowRoot ? current.parentNode.host : null);
1078
+ current = current.parentElement ?? (current.parentNode && current.parentNode.host ? current.parentNode.host : null);
1059
1079
  }
1060
1080
  };
1061
1081
  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]];
1082
+ const points = [[window.innerWidth / 2, window.innerHeight / 2], [window.innerWidth / 2, window.innerHeight * 0.7], [window.innerWidth * 0.7, window.innerHeight * 0.4]];
1063
1083
  for (const point of points) {
1064
1084
  try { addAncestors(document.elementFromPoint(point[0], point[1])); } catch { /* ignore */ }
1065
1085
  }
1066
1086
  }
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
- }
1087
+ for (const root of searchRoots()) {
1088
+ let all = [];
1089
+ try { all = root.querySelectorAll ? [...root.querySelectorAll('*')] : []; } catch { continue; }
1090
+ for (const el of all) if (isScrollableElement(el)) found.add(el);
1073
1091
  }
1074
1092
  let best = null;
1093
+ let bestArea = 0;
1075
1094
  for (const el of found) {
1076
- if (best === null || el.clientHeight * el.scrollHeight > best.clientHeight * best.scrollHeight) best = el;
1095
+ const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
1096
+ const area = rect === null ? 0 : rect.width * rect.height;
1097
+ if (best === null || area > bestArea) { best = el; bestArea = area; }
1077
1098
  }
1078
1099
  return best;
1079
1100
  }
1080
- function scrollChatTurns() {
1081
- 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;
1101
+ /**
1102
+ * The shell virtualises history: scrolling up stops at the loaded window and only the "load
1103
+ * earlier" row fetches more. A scroll control that ignores that row can never reach the top, which
1104
+ * is exactly why the previous version looked like a no-op. Find and press it before scrolling.
1105
+ */
1106
+ function findLoadEarlierButton() {
1107
+ if (typeof document === 'undefined') return null;
1108
+ for (const root of searchRoots()) {
1109
+ let nodes = [];
1110
+ try { nodes = root.querySelectorAll ? [...root.querySelectorAll('button, [role="button"], a, span, div')] : []; } catch { continue; }
1111
+ for (const el of nodes) {
1112
+ const text = String(el.textContent ?? '').trim();
1113
+ if (text === '' || text.length > 24) continue;
1114
+ if (!/加载更早|加载更多|更早|load earlier|load more/i.test(text)) continue;
1115
+ const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
1116
+ if (rect === null || rect.width < 8 || rect.height < 8) continue;
1117
+ if (typeof el.click === 'function') return el;
1118
+ }
1088
1119
  }
1089
- if (typeof window !== 'undefined') window.scrollBy({ top: -step, behavior: 'smooth' });
1120
+ return null;
1121
+ }
1122
+ function clickLoadEarlier() {
1123
+ const el = findLoadEarlierButton();
1124
+ if (el === null) return false;
1125
+ try { el.click(); return true; } catch { return false; }
1090
1126
  }
1091
- function scrollChatTop() {
1127
+ const waitFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
1128
+ async function scrollChatTurns() {
1092
1129
  const surface = findScrollSurface();
1093
- if (surface !== null) {
1094
- surface.scrollTop = 0;
1095
- surface.scrollTo({ top: 0, behavior: 'smooth' });
1096
- return;
1130
+ const viewport = typeof window !== 'undefined' ? window.innerHeight : 800;
1131
+ const step = Math.round(Math.max(240, (surface !== null ? surface.clientHeight : viewport) * 1.8));
1132
+ if (surface !== null && surface.scrollTop <= 8 && clickLoadEarlier()) await waitFor(350);
1133
+ const after = surface ?? findScrollSurface();
1134
+ if (after !== null) after.scrollTop = Math.max(0, after.scrollTop - step);
1135
+ else if (typeof window !== 'undefined') window.scrollBy({ top: -step, behavior: 'smooth' });
1136
+ }
1137
+ async function scrollChatTop() {
1138
+ // Load history until the "load earlier" row is gone, then pin the surface to the top.
1139
+ for (let i = 0; i < 60; i += 1) {
1140
+ const surface = findScrollSurface();
1141
+ if (surface !== null) surface.scrollTop = 0;
1142
+ if (!clickLoadEarlier()) break;
1143
+ await waitFor(280);
1144
+ }
1145
+ for (let i = 0; i < 8; i += 1) {
1146
+ const surface = findScrollSurface();
1147
+ if (surface !== null) surface.scrollTop = 0;
1148
+ else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
1149
+ await waitFor(70);
1097
1150
  }
1098
- if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
1099
1151
  }
1100
- /** One floating button, styled after the shell's own back-to-top control. */
1152
+ /** One black circular button, anchored to the conversation column's top-right corner. */
1101
1153
  function installFloatingScrollControl() {
1102
1154
  if (typeof document === 'undefined' || document.body === null) return null;
1103
1155
  const wrap = document.createElement('div');
@@ -1106,17 +1158,15 @@ function installFloatingScrollControl() {
1106
1158
  const button = document.createElement('button');
1107
1159
  button.type = 'button';
1108
1160
  button.className = 'w2mScrollFloatBtn';
1109
- button.title = '向上滚动 3 轮(长按 3 秒回到第一轮)';
1110
- button.setAttribute('aria-label', button.title);
1161
+ button.title = '向上滚动 3 轮(连续点击 3 次回到第一轮)';
1162
+ button.setAttribute('aria-label', '向上滚动');
1163
+ button.setAttribute('aria-description', '单次点击向上滚动三轮,连续点击三次回到第一轮');
1111
1164
  button.addEventListener('contextmenu', (event) => event.preventDefault());
1112
1165
  const ns = 'http://www.w3.org/2000/svg';
1113
1166
  const svg = document.createElementNS(ns, 'svg');
1114
1167
  svg.setAttribute('viewBox', '0 0 24 24');
1115
- svg.setAttribute('width', '18');
1116
- svg.setAttribute('height', '18');
1117
1168
  svg.setAttribute('fill', 'none');
1118
1169
  svg.setAttribute('stroke', 'currentColor');
1119
- svg.setAttribute('stroke-width', '2');
1120
1170
  svg.setAttribute('stroke-linecap', 'round');
1121
1171
  svg.setAttribute('stroke-linejoin', 'round');
1122
1172
  const shaft = document.createElementNS(ns, 'path');
@@ -1126,20 +1176,54 @@ function installFloatingScrollControl() {
1126
1176
  svg.appendChild(shaft);
1127
1177
  svg.appendChild(head);
1128
1178
  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);
1179
+ let clicks = 0;
1180
+ let resetTimer = null;
1181
+ const reset = () => { if (resetTimer !== null) { clearTimeout(resetTimer); resetTimer = null; } clicks = 0; };
1182
+ button.addEventListener('click', () => {
1183
+ clicks += 1;
1184
+ if (resetTimer !== null) clearTimeout(resetTimer);
1185
+ if (clicks >= 3) { reset(); void scrollChatTop(); return; }
1186
+ void scrollChatTurns();
1187
+ resetTimer = setTimeout(reset, 1200);
1137
1188
  });
1138
- for (const type of ['pointerup', 'pointerleave', 'pointercancel']) button.addEventListener(type, clearHold);
1139
- button.addEventListener('click', () => { if (longFired) { longFired = false; return; } scrollChatTurns(); });
1140
1189
  wrap.appendChild(button);
1141
1190
  document.body.appendChild(wrap);
1142
- return () => { clearHold(); wrap.remove(); };
1191
+ const place = () => {
1192
+ const surface = findScrollSurface();
1193
+ if (surface === null) {
1194
+ wrap.style.top = '120px';
1195
+ wrap.style.right = '16px';
1196
+ return;
1197
+ }
1198
+ const rect = surface.getBoundingClientRect();
1199
+ wrap.style.top = Math.max(8, Math.round(rect.top + 8)) + 'px';
1200
+ wrap.style.right = Math.max(12, Math.round(window.innerWidth - rect.right)) + 'px';
1201
+ };
1202
+ place();
1203
+ for (const delay of [120, 600, 1500]) setTimeout(place, delay);
1204
+ window.addEventListener('resize', place);
1205
+ let observer = null;
1206
+ let observed = null;
1207
+ const observe = () => {
1208
+ if (typeof ResizeObserver !== 'function') return;
1209
+ const surface = findScrollSurface();
1210
+ if (surface === observed) return;
1211
+ if (observer !== null) observer.disconnect();
1212
+ observed = surface;
1213
+ observer = new ResizeObserver(() => place());
1214
+ if (surface !== null) observer.observe(surface);
1215
+ };
1216
+ observe();
1217
+ const reObserve = setTimeout(observe, 800);
1218
+ const poll = setInterval(place, 1000);
1219
+ return () => {
1220
+ reset();
1221
+ clearTimeout(reObserve);
1222
+ clearInterval(poll);
1223
+ window.removeEventListener('resize', place);
1224
+ if (observer !== null) observer.disconnect();
1225
+ wrap.remove();
1226
+ };
1143
1227
  }
1144
1228
  function ConversationSearch() {
1145
1229
  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.3",
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.3",
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.3';
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;