@twinsearth/w2m-dsh-plugin 2.0.5 → 2.0.6
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 +1787 -1779
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.0.6/RELEASE-2060.json +22 -0
- package/lib/client.js +73 -15
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/plugin/dataplane-sink-route.mjs +6 -1
- package/src/plugin/tools.mjs +1 -1
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.6",
|
|
3
|
+
"case": "RELEASE-2060",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T17:02:50.164Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-2060-1793206970165",
|
|
11
|
+
"requirement": "v2.0.6:CI 跨平台修复(realpath 归一) + 搜索规则(纯数字≤100拒绝/视口最多6条/滚动刷新) + 一键到顶等待加载",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"preflight": "16 pass / 0 fail / 1 unverifiable (GREEN)",
|
|
16
|
+
"tests": "declared suite all pass; client-bundle 27/27",
|
|
17
|
+
"lint": "0 errors",
|
|
18
|
+
"pack": "byte-identical; manifest equals tree",
|
|
19
|
+
"scope": "local code + tests only; release follows via tag v2.0.6"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -1100,12 +1100,30 @@ function clearSearchMarks() {
|
|
|
1100
1100
|
if (parent.normalize) parent.normalize();
|
|
1101
1101
|
}
|
|
1102
1102
|
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Search rules (v2.0.5 UI): a pure number up to 100 is not a useful query in a conversation full of
|
|
1105
|
+
* ids/ports/timestamps, so it is refused outright (no marks, no scroll, no count). Only the viewport
|
|
1106
|
+
* is highlighted, at most six matches per screen; scrolling re-runs the pass for the new viewport.
|
|
1107
|
+
*/
|
|
1108
|
+
const SEARCH_MAX_MARKS = 6;
|
|
1109
|
+
function searchQueryAllowed(query) {
|
|
1110
|
+
const text = String(query ?? '').trim();
|
|
1111
|
+
if (text === '') return false;
|
|
1112
|
+
if (/^\d+$/.test(text)) return Number(text) > 100;
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
function viewportRect() {
|
|
1116
|
+
const width = typeof window !== 'undefined' ? window.innerWidth : 1024;
|
|
1117
|
+
const height = typeof window !== 'undefined' ? window.innerHeight : 768;
|
|
1118
|
+
return { left: 0, top: 0, right: width, bottom: height };
|
|
1119
|
+
}
|
|
1103
1120
|
function paintSearchMarks(query) {
|
|
1104
1121
|
clearSearchMarks();
|
|
1105
|
-
if (typeof document === 'undefined' || query === '') return 0;
|
|
1122
|
+
if (typeof document === 'undefined' || query === '' || !searchQueryAllowed(query)) return 0;
|
|
1106
1123
|
const needle = query.toLowerCase();
|
|
1124
|
+
const view = viewportRect();
|
|
1107
1125
|
let total = 0;
|
|
1108
|
-
for (const root of searchRoots()) {
|
|
1126
|
+
outer: for (const root of searchRoots()) {
|
|
1109
1127
|
let walker;
|
|
1110
1128
|
try {
|
|
1111
1129
|
walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
@@ -1124,21 +1142,28 @@ function paintSearchMarks(query) {
|
|
|
1124
1142
|
let node = walker.nextNode();
|
|
1125
1143
|
while (node !== null) { nodes.push(node); node = walker.nextNode(); }
|
|
1126
1144
|
for (const textNode of nodes) {
|
|
1145
|
+
// Cheap pre-filter: a text node whose element is entirely outside the viewport cannot paint.
|
|
1146
|
+
const host = textNode.parentElement;
|
|
1147
|
+
const hostRect = host !== null && typeof host.getBoundingClientRect === 'function' ? host.getBoundingClientRect() : null;
|
|
1148
|
+
if (hostRect !== null && (hostRect.bottom < view.top || hostRect.top > view.bottom)) continue;
|
|
1127
1149
|
const text = textNode.nodeValue ?? '';
|
|
1128
1150
|
const lower = text.toLowerCase();
|
|
1129
1151
|
const hits = [];
|
|
1130
1152
|
let index = lower.indexOf(needle);
|
|
1131
1153
|
while (index >= 0) { hits.push(index); index = lower.indexOf(needle, index + query.length); }
|
|
1132
|
-
for (
|
|
1154
|
+
for (const hit of hits) {
|
|
1133
1155
|
try {
|
|
1134
1156
|
const range = document.createRange();
|
|
1135
|
-
range.setStart(textNode,
|
|
1136
|
-
range.setEnd(textNode,
|
|
1157
|
+
range.setStart(textNode, hit);
|
|
1158
|
+
range.setEnd(textNode, hit + query.length);
|
|
1159
|
+
const rect = typeof range.getBoundingClientRect === 'function' ? range.getBoundingClientRect() : null;
|
|
1160
|
+
if (rect !== null && (rect.bottom < view.top || rect.top > view.bottom || rect.right < view.left || rect.left > view.right)) continue;
|
|
1137
1161
|
const mark = document.createElement('mark');
|
|
1138
1162
|
mark.className = 'w2mSearchMark';
|
|
1139
1163
|
mark.setAttribute('style', 'background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)');
|
|
1140
1164
|
range.surroundContents(mark);
|
|
1141
1165
|
total += 1;
|
|
1166
|
+
if (total >= SEARCH_MAX_MARKS) break outer;
|
|
1142
1167
|
} catch { /* a range that crosses elements cannot be wrapped; skip it */ }
|
|
1143
1168
|
}
|
|
1144
1169
|
}
|
|
@@ -1381,24 +1406,54 @@ function ConversationSearch() {
|
|
|
1381
1406
|
React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
|
|
1382
1407
|
// v2.0.5: wait until 3s pass without new input before walking the conversation, so typing does
|
|
1383
1408
|
// not trigger a full-tree highlight pass on every keystroke (the reported input jank).
|
|
1384
|
-
|
|
1385
|
-
|
|
1409
|
+
// Rules: a pure number <= 100 is refused; only the viewport is highlighted (max 6) and scrolling
|
|
1410
|
+
// repaints the current viewport without yanking the scroll position.
|
|
1411
|
+
const appliedRef = React.useRef('');
|
|
1412
|
+
const paintRef = React.useRef(() => {});
|
|
1413
|
+
paintRef.current = (focus = false) => {
|
|
1414
|
+
const total = paintSearchMarks(appliedRef.current);
|
|
1386
1415
|
setCount(total);
|
|
1387
1416
|
setCursor(total > 0 ? 0 : -1);
|
|
1388
|
-
|
|
1389
|
-
|
|
1417
|
+
if (focus === true) {
|
|
1418
|
+
const first = document.querySelector('mark.w2mSearchMark');
|
|
1419
|
+
if (first && first.scrollIntoView) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1420
|
+
}
|
|
1390
1421
|
};
|
|
1391
1422
|
React.useEffect(() => {
|
|
1392
|
-
if (!open) { clearSearchMarks(); setCount(0); setCursor(-1); }
|
|
1423
|
+
if (!open) { appliedRef.current = ''; clearSearchMarks(); setCount(0); setCursor(-1); }
|
|
1393
1424
|
}, [open]);
|
|
1394
1425
|
React.useEffect(() => {
|
|
1395
1426
|
if (!open) return undefined;
|
|
1396
|
-
|
|
1427
|
+
if (!searchQueryAllowed(query)) {
|
|
1428
|
+
// Refused query: no marks, no count, no scroll — the input itself stays usable.
|
|
1429
|
+
appliedRef.current = '';
|
|
1430
|
+
clearSearchMarks();
|
|
1431
|
+
setCount(0);
|
|
1432
|
+
setCursor(-1);
|
|
1433
|
+
return undefined;
|
|
1434
|
+
}
|
|
1435
|
+
const timer = setTimeout(() => { appliedRef.current = query; paintRef.current(true); }, 3000);
|
|
1397
1436
|
return () => clearTimeout(timer);
|
|
1398
1437
|
}, [open, query]);
|
|
1438
|
+
React.useEffect(() => {
|
|
1439
|
+
if (!open) return undefined;
|
|
1440
|
+
let timer = null;
|
|
1441
|
+
const onScroll = () => {
|
|
1442
|
+
if (appliedRef.current === '') return;
|
|
1443
|
+
if (timer !== null) clearTimeout(timer);
|
|
1444
|
+
timer = setTimeout(() => { paintRef.current(false); }, 150);
|
|
1445
|
+
};
|
|
1446
|
+
window.addEventListener('scroll', onScroll, true);
|
|
1447
|
+
return () => {
|
|
1448
|
+
if (timer !== null) clearTimeout(timer);
|
|
1449
|
+
window.removeEventListener('scroll', onScroll, true);
|
|
1450
|
+
};
|
|
1451
|
+
}, [open]);
|
|
1399
1452
|
const step = (backwards) => {
|
|
1400
|
-
|
|
1401
|
-
|
|
1453
|
+
if (appliedRef.current === '') return;
|
|
1454
|
+
let marks = [...document.querySelectorAll('mark.w2mSearchMark')];
|
|
1455
|
+
if (marks.length === 0) { paintRef.current(false); marks = [...document.querySelectorAll('mark.w2mSearchMark')]; }
|
|
1456
|
+
if (marks.length === 0) return;
|
|
1402
1457
|
const next = (cursor + (backwards ? -1 : 1) + marks.length) % marks.length;
|
|
1403
1458
|
setCursor(next);
|
|
1404
1459
|
if (marks[next] && marks[next].scrollIntoView) marks[next].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
@@ -1424,11 +1479,14 @@ function ConversationSearch() {
|
|
|
1424
1479
|
placeholder: '搜索本对话全部内容',
|
|
1425
1480
|
onChange: (event) => setQuery(event.target.value),
|
|
1426
1481
|
onKeyDown: (event) => {
|
|
1427
|
-
if (event.key === 'Enter') {
|
|
1482
|
+
if (event.key === 'Enter') {
|
|
1483
|
+
event.preventDefault();
|
|
1484
|
+
if (searchQueryAllowed(query)) { appliedRef.current = query; paintRef.current(true); step(event.shiftKey); }
|
|
1485
|
+
}
|
|
1428
1486
|
if (event.key === 'Escape') { setOpen(false); setQuery(''); }
|
|
1429
1487
|
},
|
|
1430
1488
|
}),
|
|
1431
|
-
h('span', { key: 'count', className: 'w2mSearchCount' }, count === 0 ? (query
|
|
1489
|
+
h('span', { key: 'count', className: 'w2mSearchCount' }, count === 0 ? (searchQueryAllowed(query) ? '0' : '') : whenCount(count, cursor)),
|
|
1432
1490
|
h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => step(true) }, '↑'),
|
|
1433
1491
|
h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => step(false) }, '↓'),
|
|
1434
1492
|
h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); clearSearchMarks(); } }, '×'),
|
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.6",
|
|
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.6",
|
|
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": [
|
|
@@ -28,7 +28,12 @@ function readConfig(stateDir) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function insideStateDir(stateDir, target) {
|
|
31
|
-
|
|
31
|
+
// Compare canonical paths: Windows temp dirs come back as RUNNER~1 here and runneradmin in the
|
|
32
|
+
// realpath, and macOS /var is a symlink to /private/var, so a raw prefix test lets an inside path
|
|
33
|
+
// through on CI even though it is inside the state dir.
|
|
34
|
+
let base = stateDir;
|
|
35
|
+
try { base = realpathSync.native ? realpathSync.native(stateDir) : realpathSync(stateDir); } catch { /* keep raw */ }
|
|
36
|
+
const rel = relative(base, target);
|
|
32
37
|
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
33
38
|
}
|
|
34
39
|
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -289,7 +289,7 @@ const MAX_STAGES = 16;
|
|
|
289
289
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
290
290
|
* overrides it at runtime for tests and packagers.
|
|
291
291
|
*/
|
|
292
|
-
const PLUGIN_VERSION = '2.0.
|
|
292
|
+
const PLUGIN_VERSION = '2.0.6';
|
|
293
293
|
|
|
294
294
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
295
295
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|