@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +10 -0
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.0.3/RELEASE-2030.json +22 -0
- package/lib/client.js +90 -43
- 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.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.
|
|
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.
|
|
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.
|
|
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 官方浮动滚动控件对齐(同右边缘、位于其上方;官方控件出现前后都会重新对齐)。
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -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:
|
|
511
|
-
'.w2mScrollFloatBtn{width:
|
|
512
|
-
'.w2mScrollFloatBtn:hover{
|
|
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
|
-
*
|
|
1047
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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,
|
|
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',
|
|
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) {
|
|
1145
|
-
scrollChatTurns();
|
|
1146
|
-
resetTimer = setTimeout(
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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 =
|
|
1168
|
-
wrap.style.
|
|
1169
|
-
wrap.style.
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
const
|
|
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
|
-
|
|
1177
|
-
|
|
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.
|
|
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.
|
|
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": [
|
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.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;
|