@twinsearth/w2m-dsh-plugin 2.0.0 → 2.0.1

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.0"
8
+ "version": "2.0.1"
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.0",
15
+ "version": "2.0.1",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "2.0.0"
29
+ "version": "2.0.1"
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.0",
4
+ "version": "2.0.1",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [2.0.1] - 2026-10-28
11
+
12
+ - 修复右上角滚动按钮无效:滚动容器识别改为“命中测试 + 递归 shadow root + 允许 overflow:hidden 的程序化滚动容器”,
13
+ 并直接写 scrollTop/scrollTo;找不到容器时回退到 window。旧实现只找 overflow:auto,虚拟列表/隐藏溢出容器下点击是空操作。
14
+ - 只保留一个滚动图标(SVG 向上箭头,样式与右上角官方图标对齐:32px、8px 圆角、1px 边框、right:16px/top:64px),
15
+ 删除原来的第二个“一键到顶”按钮与会话头部的重复“⇧3”按钮。
16
+ - 交互:点一次向上滚动约 3 轮;长按 3 秒直接回到第一轮(长按后松手的 click 不会重复触发)。
17
+
10
18
  ## [2.0.0] - 2026-10-28
11
19
 
12
20
  主插件与既有 Relay 的对接按《W2M Relay × ToFuture 外部对接 API v1.2》收敛;ToFuture 子插件按该文档
@@ -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.0", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@2.0.1", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "2.0.1",
3
+ "case": "RELEASE-2010",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-28T09:49:39.826Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-2010-1793180979827",
11
+ "requirement": "v2.0.1:右上角单个滚动图标,点击滚动 3 轮、长按 3 秒到顶,图标与官方一键到顶对齐,修复点击无效",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "single_icon": "one SVG button; second top button and header duplicate removed",
16
+ "interaction": "click -> scrollTop -1.8*clientHeight; long press 3s -> top; click after long press suppressed",
17
+ "resolver": "elementFromPoint ancestors + shadow roots + overflow hidden + scrollTop direct write + window fallback",
18
+ "alignment": "right:16px, top:64px, 32x32, radius 8px, 1px border",
19
+ "tests": "client-bundle 26/26, lint 0 errors"
20
+ }
21
+ }
22
+ }
package/lib/client.js CHANGED
@@ -507,10 +507,9 @@ 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:18px;top:72px;z-index:2147483000;display:flex;flex-direction:column;gap:6px}',
511
- '.w2mScrollFloatBtn{width:34px;height:34px;border-radius:50%;border:1px solid currentColor;background:transparent;color:inherit;opacity:.72;font-size:13px;line-height:1;cursor:pointer}',
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
512
  '.w2mScrollFloatBtn:hover{opacity:1}',
513
- '.w2mScrollUp{font-size:12px;padding:2px 6px;border:1px solid rgba(128,128,128,.4);border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
514
513
  '::highlight(w2m-conversation-search){background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)}',
515
514
  '.w2mSearchMark{background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1);border-radius:2px}',
516
515
  '.w2mSearchBtn{height:24px;min-width:24px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
@@ -1041,62 +1040,104 @@ function paintSearchMarks(query) {
1041
1040
  }
1042
1041
  return total;
1043
1042
  }
1043
+ function isScrollableElement(el) {
1044
+ 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;
1046
+ 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';
1050
+ }
1044
1051
  function findScrollSurface() {
1045
1052
  if (typeof document === 'undefined') return null;
1046
- let best = null;
1047
- for (const root of searchRoots()) {
1048
- let all = [];
1049
- try { all = root.querySelectorAll ? [...root.querySelectorAll('*')] : []; } catch { continue; }
1050
- for (const el of all) {
1051
- if (el.scrollHeight <= el.clientHeight + 40) continue;
1052
- const style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
1053
- const overflow = style === null ? '' : style.overflowY;
1054
- if (overflow !== 'auto' && overflow !== 'scroll') continue;
1055
- if (best === null || el.scrollHeight > best.scrollHeight) best = el;
1053
+ const found = new Set();
1054
+ const addAncestors = (node) => {
1055
+ let current = node;
1056
+ while (current !== null && current !== undefined) {
1057
+ if (isScrollableElement(current)) { found.add(current); return; }
1058
+ current = current.parentElement ?? (current.parentNode instanceof ShadowRoot ? current.parentNode.host : null);
1059
+ }
1060
+ };
1061
+ 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]];
1063
+ for (const point of points) {
1064
+ try { addAncestors(document.elementFromPoint(point[0], point[1])); } catch { /* ignore */ }
1056
1065
  }
1057
1066
  }
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
+ }
1073
+ }
1074
+ let best = null;
1075
+ for (const el of found) {
1076
+ if (best === null || el.clientHeight * el.scrollHeight > best.clientHeight * best.scrollHeight) best = el;
1077
+ }
1058
1078
  return best;
1059
1079
  }
1060
1080
  function scrollChatTurns() {
1061
1081
  const surface = findScrollSurface();
1062
- if (surface !== null) surface.scrollBy({ top: -Math.round(Math.max(240, surface.clientHeight * 1.8)), behavior: 'smooth' });
1063
- else if (typeof window !== 'undefined') window.scrollBy({ top: -Math.round(window.innerHeight * 1.8), behavior: 'smooth' });
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' });
1064
1090
  }
1065
1091
  function scrollChatTop() {
1066
1092
  const surface = findScrollSurface();
1067
- if (surface !== null) surface.scrollTo({ top: 0, behavior: 'smooth' });
1068
- else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
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' });
1069
1099
  }
1070
- /** Floating scroll controls: fixed top-right of the workspace, independent of any slot layout. */
1071
- function installFloatingScrollButtons() {
1100
+ /** One floating button, styled after the shell's own back-to-top control. */
1101
+ function installFloatingScrollControl() {
1072
1102
  if (typeof document === 'undefined' || document.body === null) return null;
1073
1103
  const wrap = document.createElement('div');
1074
1104
  wrap.className = 'w2mScrollFloatWrap';
1075
1105
  wrap.setAttribute('data-w2m-scroll-float', 'true');
1076
- const makeButton = (label, title) => {
1077
- const button = document.createElement('button');
1078
- button.type = 'button';
1079
- button.className = 'w2mScrollFloatBtn';
1080
- button.textContent = label;
1081
- button.title = title;
1082
- button.setAttribute('aria-label', title);
1083
- wrap.appendChild(button);
1084
- return button;
1085
- };
1086
- const up = makeButton('\u21E73', '向上滚动 3 轮(长按 3 秒到第一轮)');
1087
- const top = makeButton('\u2912', '一键到顶');
1106
+ const button = document.createElement('button');
1107
+ button.type = 'button';
1108
+ button.className = 'w2mScrollFloatBtn';
1109
+ button.title = '向上滚动 3 轮(长按 3 秒回到第一轮)';
1110
+ button.setAttribute('aria-label', button.title);
1111
+ button.addEventListener('contextmenu', (event) => event.preventDefault());
1112
+ const ns = 'http://www.w3.org/2000/svg';
1113
+ const svg = document.createElementNS(ns, 'svg');
1114
+ svg.setAttribute('viewBox', '0 0 24 24');
1115
+ svg.setAttribute('width', '18');
1116
+ svg.setAttribute('height', '18');
1117
+ svg.setAttribute('fill', 'none');
1118
+ svg.setAttribute('stroke', 'currentColor');
1119
+ svg.setAttribute('stroke-width', '2');
1120
+ svg.setAttribute('stroke-linecap', 'round');
1121
+ svg.setAttribute('stroke-linejoin', 'round');
1122
+ const shaft = document.createElementNS(ns, 'path');
1123
+ shaft.setAttribute('d', 'M12 19V5');
1124
+ const head = document.createElementNS(ns, 'path');
1125
+ head.setAttribute('d', 'M5 12l7-7 7 7');
1126
+ svg.appendChild(shaft);
1127
+ svg.appendChild(head);
1128
+ button.appendChild(svg);
1088
1129
  let holdTimer = null;
1089
1130
  let longFired = false;
1090
1131
  const clearHold = () => { if (holdTimer !== null) { clearTimeout(holdTimer); holdTimer = null; } };
1091
- up.addEventListener('pointerdown', (event) => {
1132
+ button.addEventListener('pointerdown', (event) => {
1092
1133
  if (event && typeof event.preventDefault === 'function') event.preventDefault();
1093
1134
  longFired = false;
1094
1135
  clearHold();
1095
1136
  holdTimer = setTimeout(() => { longFired = true; scrollChatTop(); }, 3000);
1096
1137
  });
1097
- for (const type of ['pointerup', 'pointerleave', 'pointercancel']) up.addEventListener(type, clearHold);
1098
- up.addEventListener('click', () => { if (longFired) { longFired = false; return; } scrollChatTurns(); });
1099
- top.addEventListener('click', () => scrollChatTop());
1138
+ for (const type of ['pointerup', 'pointerleave', 'pointercancel']) button.addEventListener(type, clearHold);
1139
+ button.addEventListener('click', () => { if (longFired) { longFired = false; return; } scrollChatTurns(); });
1140
+ wrap.appendChild(button);
1100
1141
  document.body.appendChild(wrap);
1101
1142
  return () => { clearHold(); wrap.remove(); };
1102
1143
  }
@@ -1106,8 +1147,6 @@ function ConversationSearch() {
1106
1147
  const [count, setCount] = React.useState(0);
1107
1148
  const [cursor, setCursor] = React.useState(-1);
1108
1149
  const inputRef = React.useRef(null);
1109
- const holdRef = React.useRef(null);
1110
- const longFiredRef = React.useRef(false);
1111
1150
  React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
1112
1151
  React.useEffect(() => {
1113
1152
  if (!open) { clearSearchMarks(); setCount(0); setCursor(-1); return undefined; }
@@ -1125,32 +1164,8 @@ function ConversationSearch() {
1125
1164
  setCursor(next);
1126
1165
  if (marks[next] && marks[next].scrollIntoView) marks[next].scrollIntoView({ block: 'center', behavior: 'smooth' });
1127
1166
  };
1128
- const scrollTurns = () => {
1129
- const surface = findScrollSurface();
1130
- if (surface !== null) surface.scrollBy({ top: -Math.round(Math.max(240, surface.clientHeight * 1.8)), behavior: 'smooth' });
1131
- else if (typeof window !== 'undefined') window.scrollBy({ top: -Math.round(window.innerHeight * 1.8), behavior: 'smooth' });
1132
- };
1133
- const scrollFirstTurn = () => {
1134
- const surface = findScrollSurface();
1135
- if (surface !== null) surface.scrollTo({ top: 0, behavior: 'smooth' });
1136
- else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
1137
- };
1138
1167
  if (!open) {
1139
1168
  return h('span', { className: 'w2mSearchControls' }, [
1140
- h('button', {
1141
- key: 'up',
1142
- type: 'button',
1143
- className: 'w2mScrollUp',
1144
- title: '向上滚动 3 轮(长按 3 秒到第一轮)',
1145
- 'aria-label': '向上滚动 3 轮,长按 3 秒到第一轮',
1146
- onMouseDown: () => {
1147
- longFiredRef.current = false;
1148
- holdRef.current = setTimeout(() => { longFiredRef.current = true; scrollFirstTurn(); }, 3000);
1149
- },
1150
- onMouseUp: () => { if (holdRef.current) { clearTimeout(holdRef.current); holdRef.current = null; } },
1151
- onMouseLeave: () => { if (holdRef.current) { clearTimeout(holdRef.current); holdRef.current = null; } },
1152
- onClick: () => { if (longFiredRef.current) { longFiredRef.current = false; return; } scrollTurns(); },
1153
- }, '⇧3'),
1154
1169
  h('button', {
1155
1170
  key: 'find',
1156
1171
  type: 'button',
@@ -2417,7 +2432,7 @@ const CHAT_EMOJIS = CHAT_COMMANDS;
2417
2432
  // v1.9.20: floating scroll controls, fixed top-right of the workspace. DOM nodes, not slot
2418
2433
  // entries, so they appear whichever conversation view is active; ctx.effect disposes them.
2419
2434
  try {
2420
- const disposeFloat = installFloatingScrollButtons();
2435
+ const disposeFloat = installFloatingScrollControl();
2421
2436
  if (typeof disposeFloat === 'function' && typeof ctx.effect === 'function') {
2422
2437
  ctx.effect(() => disposeFloat, 'w2m: floating scroll buttons');
2423
2438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
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.0",
4
+ "version": "2.0.1",
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.0';
289
+ const PLUGIN_VERSION = '2.0.1';
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;