@twinsearth/w2m-dsh-plugin 2.0.2 → 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.2"
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.2",
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.2"
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.2",
4
+ "version": "2.0.3",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@
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
+
10
20
  ## [2.0.2] - 2026-10-28
11
21
 
12
22
  - 滚动按钮移到右下角,与 DSH 官方浮动滚动控件对齐(同右边缘、位于其上方;官方控件出现前后都会重新对齐)。
@@ -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.2", "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,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;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
- '.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}',
@@ -1043,9 +1046,9 @@ function paintSearchMarks(query) {
1043
1046
  /**
1044
1047
  * Probe-scroll: assign a tiny scrollTop and see whether the browser keeps it.
1045
1048
  *
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
+ * 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.
1049
1052
  */
1050
1053
  function isScrollableElement(el) {
1051
1054
  if (el === null || el === undefined || el === document.body || el === document.documentElement) return false;
@@ -1095,19 +1098,58 @@ function findScrollSurface() {
1095
1098
  }
1096
1099
  return best;
1097
1100
  }
1098
- function scrollChatTurns() {
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
+ }
1119
+ }
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; }
1126
+ }
1127
+ const waitFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
1128
+ async function scrollChatTurns() {
1099
1129
  const surface = findScrollSurface();
1100
1130
  const viewport = typeof window !== 'undefined' ? window.innerHeight : 800;
1101
1131
  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);
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);
1103
1135
  else if (typeof window !== 'undefined') window.scrollBy({ top: -step, behavior: 'smooth' });
1104
1136
  }
1105
- function scrollChatTop() {
1106
- const surface = findScrollSurface();
1107
- if (surface !== null) surface.scrollTop = 0;
1108
- else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
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);
1150
+ }
1109
1151
  }
1110
- /** One circular button, aligned with the shell's own floating scroll control. */
1152
+ /** One black circular button, anchored to the conversation column's top-right corner. */
1111
1153
  function installFloatingScrollControl() {
1112
1154
  if (typeof document === 'undefined' || document.body === null) return null;
1113
1155
  const wrap = document.createElement('div');
@@ -1117,16 +1159,14 @@ function installFloatingScrollControl() {
1117
1159
  button.type = 'button';
1118
1160
  button.className = 'w2mScrollFloatBtn';
1119
1161
  button.title = '向上滚动 3 轮(连续点击 3 次回到第一轮)';
1120
- button.setAttribute('aria-label', button.title);
1162
+ button.setAttribute('aria-label', '向上滚动');
1163
+ button.setAttribute('aria-description', '单次点击向上滚动三轮,连续点击三次回到第一轮');
1121
1164
  button.addEventListener('contextmenu', (event) => event.preventDefault());
1122
1165
  const ns = 'http://www.w3.org/2000/svg';
1123
1166
  const svg = document.createElementNS(ns, 'svg');
1124
1167
  svg.setAttribute('viewBox', '0 0 24 24');
1125
- svg.setAttribute('width', '18');
1126
- svg.setAttribute('height', '18');
1127
1168
  svg.setAttribute('fill', 'none');
1128
1169
  svg.setAttribute('stroke', 'currentColor');
1129
- svg.setAttribute('stroke-width', '2');
1130
1170
  svg.setAttribute('stroke-linecap', 'round');
1131
1171
  svg.setAttribute('stroke-linejoin', 'round');
1132
1172
  const shaft = document.createElementNS(ns, 'path');
@@ -1138,43 +1178,50 @@ function installFloatingScrollControl() {
1138
1178
  button.appendChild(svg);
1139
1179
  let clicks = 0;
1140
1180
  let resetTimer = null;
1181
+ const reset = () => { if (resetTimer !== null) { clearTimeout(resetTimer); resetTimer = null; } clicks = 0; };
1141
1182
  button.addEventListener('click', () => {
1142
1183
  clicks += 1;
1143
1184
  if (resetTimer !== null) clearTimeout(resetTimer);
1144
- if (clicks >= 3) { clicks = 0; scrollChatTop(); return; }
1145
- scrollChatTurns();
1146
- resetTimer = setTimeout(() => { clicks = 0; resetTimer = null; }, 1200);
1185
+ if (clicks >= 3) { reset(); void scrollChatTop(); return; }
1186
+ void scrollChatTurns();
1187
+ resetTimer = setTimeout(reset, 1200);
1147
1188
  });
1148
1189
  wrap.appendChild(button);
1149
1190
  document.body.appendChild(wrap);
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) {
1191
+ const place = () => {
1192
+ const surface = findScrollSurface();
1193
+ if (surface === null) {
1194
+ wrap.style.top = '120px';
1163
1195
  wrap.style.right = '16px';
1164
- wrap.style.bottom = '132px';
1165
1196
  return;
1166
1197
  }
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';
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);
1170
1215
  };
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);
1216
+ observe();
1217
+ const reObserve = setTimeout(observe, 800);
1218
+ const poll = setInterval(place, 1000);
1175
1219
  return () => {
1176
- if (resetTimer !== null) clearTimeout(resetTimer);
1177
- if (typeof window !== 'undefined') window.removeEventListener('resize', onResize);
1220
+ reset();
1221
+ clearTimeout(reObserve);
1222
+ clearInterval(poll);
1223
+ window.removeEventListener('resize', place);
1224
+ if (observer !== null) observer.disconnect();
1178
1225
  wrap.remove();
1179
1226
  };
1180
1227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "2.0.2",
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.2",
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.2';
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;