@twinsearth/w2m-dsh-plugin 1.9.17 → 1.9.19

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": "1.9.17"
8
+ "version": "1.9.19"
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": "1.9.17",
15
+ "version": "1.9.19",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.9.17"
29
+ "version": "1.9.19"
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": "1.9.17",
4
+ "version": "1.9.19",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.9.19] - 2026-10-28
11
+
12
+ - 对话右上角新增「⇧3」按钮:单击向上滚动约 3 轮对话,长按 3 秒直接回到第一轮。
13
+ - 全文搜索高亮修复:改为直接插入本地 mark 元素(黄色背景 rgba(255,235,59,.85) + 绿色文字 rgba(11,122,51,1)),并递归遍历 shadow DOM;不再依赖 CSS Custom Highlight API,命中项一定可见。
14
+ - 同版带上 v1.9.18 的聊天收紧:relay 只接收文字与 /微笑 /火箭 /烟花 指令(图片字段、data:image、Markdown 图片、base64 图片魔数一律 400 拒绝;加密消息按设计豁免);表情按钮改为指令按钮;长提示词镜像按 3500 B 截断,避免被 4096 B 上限静默丢弃。
15
+
10
16
  ## [1.9.17] - 2026-10-28
11
17
 
12
18
  - 修复左侧栏 GPW 入口只剩小图标:恢复 🌐 GPW 文字(侧栏 label 仍为空,不会重复)。
package/README.md CHANGED
@@ -14,7 +14,7 @@ English | [中文](#中文说明)
14
14
 
15
15
  ## Install
16
16
 
17
- Five routes in. They all end with **16 tools registered** in your agent. Pick one.
17
+ Five routes in. They all end with **18 tools registered** in your agent. Pick one.
18
18
 
19
19
  ### 1. DSH plugin (recommended — zero configuration)
20
20
 
@@ -136,7 +136,7 @@ hand-maintained host list.
136
136
  ### Tools
137
137
 
138
138
  `w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report` ·
139
- `w2m_memory` · `w2m_approvals` · `w2m_cancel` · `w2m_council` · `w2m_chat` · `w2m_update` `w2m_tasks` · `w2m_reduce`
139
+ `w2m_memory` · `w2m_approvals` · `w2m_cancel` · `w2m_council` · `w2m_chat` · `w2m_update` `w2m_tasks` · `w2m_reduce` · `w2m_joint_plan` · `w2m_joint_status`
140
140
  - `w2m_intent` — map a structured intent (run-tests, node-version, git-status, git-head, fleet-facts, shard-probe) onto an allow-listed command; unknown intents are refused by name, never guessed.
141
141
 
142
142
  ### Environment a dispatched command receives (1.1.15+)
@@ -261,7 +261,7 @@ MIT.
261
261
 
262
262
  ### 安装
263
263
 
264
- 五种方式,最终都会在你的 agent 里注册 **16 tools registered**。任选其一。
264
+ 五种方式,最终都会在你的 agent 里注册 **18 tools registered**。任选其一。
265
265
 
266
266
  #### 1. DSH 插件(推荐,零配置)
267
267
 
package/bin/w2m-joint.mjs CHANGED
@@ -163,7 +163,7 @@ if (command === 'dispatch') {
163
163
  // decision 2: one approval authorises exactly one dispatch.
164
164
  const consumed = consumeApproval({ stateDir, planSha: args.plan, approvalSha: gate.approval.approval.approval_sha256 });
165
165
  if (consumed.ok !== true) fail(consumed.code ?? 'APPROVAL_CONSUME_FAILED', consumed.detail ?? null);
166
- const sent = await dispatchPlanToRelay({ rabbitUrl: relayUrl, token, plan: gate.plan, approval: gate.approval.approval });
166
+ const sent = await dispatchPlanToRelay({ rabbitUrl: relayUrl, token, plan: gate.plan, approval: gate.approval.approval, auditFile: stateDir + '/joint-dev/audit.jsonl' });
167
167
  print({ ...sent, plan_sha256: gate.plan.plan_sha256, relay: relayUrl });
168
168
  process.exit(sent.ok === true ? 0 : 1);
169
169
  }
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.9.17", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.9.19", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": "1.9.19",
3
+ "case": "RELEASE-1919",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-28T05:08:27.700Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-1919-1793164107702",
11
+ "requirement": "V1.9.19:对话向上滚动按钮(点击 3 轮/长按 3 秒到首轮)+ 搜索命中绿字黄底(DOM mark,含 shadow DOM)",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "scroll": "header ⇧3: click scrollBy 3 turns, hold 3s scrollTo top",
16
+ "search": "mark.w2mSearchMark with rgba yellow background + rgba green text, shadow-root aware",
17
+ "chat": "text-only guard retained from v1.9.18 work",
18
+ "tests": "client-bundle 26/26"
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "kind": "ops-smoke",
3
+ "case": "AG-SMOKE",
4
+ "executed_at": "2026-10-28T05:02:36.913Z",
5
+ "executor": "lead",
6
+ "task_id": "01M60E2W38FFKXKRZS95JSVWFM",
7
+ "setup": "relay 43.142.74.71:8787 upgraded to 1.9.16; operator token used from ~/.w2m/operator-token; read via local device token",
8
+ "attempt": "broadcast executor_machine_id=ToFuture command_argv=[\"node\",\"-v\"] at 2.0.0-tree relay",
9
+ "observed": {
10
+ "dispatch": "POST /v1/task 200; executor lease on ToFuture=offered; DSH/macmini and 15 other devices=observing",
11
+ "final": "aggregate=refused; ToFuture lease=refused (agent allow-list), outcome=refused; observers did not execute"
12
+ },
13
+ "conclusions": {
14
+ "selection_routing": "PASS at relay level: only the named executor gets an executing lease; every other device is observing",
15
+ "execution": "NOT PROVEN: the target agent refused the command (allow-list)",
16
+ "ag_matrix": "PARTIAL: A/B/C/D routing smoke only; E/F/G and real execution blocked"
17
+ },
18
+ "blockers": [
19
+ "agent allow-list does not include node -v on ToFuture",
20
+ "running DSH process still predates the operator-token change (restart required)",
21
+ "macmini/DSH plugin versions unknown/unverified; same-version requirement not yet met",
22
+ "S4 data plane (agent putBlobBytes reroute + no-sink degradation) not implemented"
23
+ ]
24
+ }
package/lib/client.js CHANGED
@@ -495,6 +495,8 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
495
495
  // v1.6.10: the transcript is selectable text; screenshots get a preview and a copy affordance.
496
496
  '.w2mChatTab{padding:4px 10px;border-radius:6px;border:1px solid rgba(127,127,127,.35);background:transparent;color:inherit;font:inherit;cursor:pointer}',
497
497
  '.w2mChat_panelFull{position:fixed;inset:5vh 8vw;width:auto;max-height:90vh;z-index:70}',
498
+ '.w2mChat_sticker{font-size:16px;line-height:1}',
499
+ '.w2mChat_stickerBig{font-size:28px;line-height:1.1}',
498
500
  '.w2mChat_emojiRow{display:flex;gap:4px;padding:4px 8px;flex-wrap:wrap}',
499
501
  '.w2mChat_emoji{padding:2px 4px;border:0;background:transparent;font-size:16px;line-height:1;cursor:pointer}',
500
502
  '.w2mFleetStatus{display:flex;gap:16px;align-items:center;padding:2px 12px;font-size:11px;opacity:.8;white-space:nowrap}',
@@ -504,6 +506,8 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
504
506
  '.w2mSearchBox{display:flex;align-items:center;gap:4px;padding:2px 4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-background-secondary)}',
505
507
  '.w2mSearchInput{height:24px;min-width:220px;border:0;background:transparent;color:inherit;font:inherit;outline:none}',
506
508
  '.w2mSearchCount{font-size:11px;opacity:.75;min-width:34px;text-align:center}',
509
+ '.w2mSearchControls{display:inline-flex;align-items:center;gap:4px}',
510
+ '.w2mScrollUp{font-size:12px;padding:2px 6px;border:1px solid rgba(128,128,128,.4);border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
507
511
  '::highlight(w2m-conversation-search){background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)}',
508
512
  '.w2mSearchMark{background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1);border-radius:2px}',
509
513
  '.w2mSearchBtn{height:24px;min-width:24px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
@@ -964,112 +968,173 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
964
968
  * Custom Highlight API (no DOM mutation, so React's tree stays intact), then scrolls to the
965
969
  * focused match. Matches stay yellow until the query changes or the box closes.
966
970
  */
967
- const SEARCH_HIGHLIGHT = 'w2m-conversation-search';
968
- function clearSearchHighlight() {
969
- try { if (typeof CSS !== 'undefined' && CSS.highlights) CSS.highlights.delete(SEARCH_HIGHLIGHT); } catch { /* unsupported */ }
970
- }
971
- function ConversationSearch() {
972
- const [open, setOpen] = React.useState(false);
973
- const [query, setQuery] = React.useState('');
974
- const [count, setCount] = React.useState(0);
975
- const [cursor, setCursor] = React.useState(-1);
976
- const rangesRef = React.useRef([]);
977
- const inputRef = React.useRef(null);
978
- React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
979
- const collect = React.useCallback(() => {
980
- if (query === '' || typeof document === 'undefined') return [];
981
- const needle = query.toLowerCase();
982
- const ranges = [];
983
- try {
984
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
985
- acceptNode(node) {
986
- const parent = node.parentElement;
987
- if (parent === null) return NodeFilter.FILTER_REJECT;
988
- const tag = parent.tagName;
989
- if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'TEXTAREA' || tag === 'INPUT') return NodeFilter.FILTER_REJECT;
990
- if (parent.closest && parent.closest('[data-w2m-search]') !== null) return NodeFilter.FILTER_REJECT;
991
- return node.nodeValue && node.nodeValue.trim() !== '' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
992
- },
993
- });
994
- let node = walker.nextNode();
995
- while (node !== null && ranges.length < 500) {
996
- const text = node.nodeValue.toLowerCase();
997
- let index = text.indexOf(needle);
998
- while (index >= 0) {
999
- const range = document.createRange();
1000
- range.setStart(node, index);
1001
- range.setEnd(node, index + query.length);
1002
- ranges.push(range);
1003
- index = text.indexOf(needle, index + query.length);
1004
- if (ranges.length >= 500) break;
1005
- }
1006
- node = walker.nextNode();
1007
- }
1008
- } catch { /* finding is best effort */ }
1009
- return ranges;
1010
- }, [query]);
1011
- const paint = React.useCallback(() => {
1012
- const ranges = collect();
1013
- rangesRef.current = ranges;
1014
- setCount(ranges.length);
1015
- try {
1016
- if (typeof CSS !== 'undefined' && CSS.highlights && typeof Highlight === 'function') {
1017
- CSS.highlights.set(SEARCH_HIGHLIGHT, new Highlight(...ranges));
1018
- }
1019
- } catch { /* older engine: the count still helps */ }
1020
- if (ranges.length > 0) {
1021
- setCursor(0);
1022
- try {
1023
- const parent = ranges[0].startContainer.parentElement;
1024
- if (parent && parent.scrollIntoView) parent.scrollIntoView({ block: 'center', behavior: 'smooth' });
1025
- } catch { /* scroll is best effort */ }
1026
- } else setCursor(-1);
1027
- return ranges.length;
1028
- }, [collect]);
1029
- React.useEffect(() => {
1030
- if (!open) { clearSearchHighlight(); setCount(0); setCursor(-1); return undefined; }
1031
- paint();
1032
- return () => clearSearchHighlight();
1033
- }, [open, query, paint]);
1034
- const step = (backwards) => {
1035
- const ranges = rangesRef.current;
1036
- if (ranges.length === 0) { paint(); return; }
1037
- const next = (cursor + (backwards ? -1 : 1) + ranges.length) % ranges.length;
1038
- setCursor(next);
971
+ function searchRoots() {
972
+ const roots = [];
973
+ const seen = new Set();
974
+ const visit = (node) => {
975
+ if (!node || seen.has(node)) return;
976
+ seen.add(node);
977
+ if (node.nodeType === 1 || node.nodeType === 11) {
978
+ if (node.shadowRoot) { roots.push(node.shadowRoot); visit(node.shadowRoot); }
979
+ const kids = node.children ?? [];
980
+ for (let i = 0; i < kids.length; i += 1) visit(kids[i]);
981
+ }
982
+ };
983
+ if (typeof document !== 'undefined' && document.body) { roots.push(document.body); visit(document.body); }
984
+ return roots;
985
+ }
986
+ function clearSearchMarks() {
987
+ if (typeof document === 'undefined') return;
988
+ const marks = document.querySelectorAll('mark.w2mSearchMark');
989
+ for (const mark of marks) {
990
+ const parent = mark.parentNode;
991
+ if (parent === null) continue;
992
+ parent.replaceChild(document.createTextNode(mark.textContent ?? ''), mark);
993
+ if (parent.normalize) parent.normalize();
994
+ }
995
+ }
996
+ function paintSearchMarks(query) {
997
+ clearSearchMarks();
998
+ if (typeof document === 'undefined' || query === '') return 0;
999
+ const needle = query.toLowerCase();
1000
+ let total = 0;
1001
+ for (const root of searchRoots()) {
1002
+ let walker;
1003
+ try {
1004
+ walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
1005
+ acceptNode(node) {
1006
+ const parent = node.parentElement;
1007
+ if (parent === null) return NodeFilter.FILTER_REJECT;
1008
+ const tag = parent.tagName;
1009
+ if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'TEXTAREA' || tag === 'INPUT') return NodeFilter.FILTER_REJECT;
1010
+ if (parent.closest && parent.closest('[data-w2m-search]') !== null) return NodeFilter.FILTER_REJECT;
1011
+ if (parent.closest && parent.closest('mark.w2mSearchMark') !== null) return NodeFilter.FILTER_REJECT;
1012
+ return node.nodeValue && node.nodeValue.trim() !== '' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
1013
+ },
1014
+ });
1015
+ } catch { continue; }
1016
+ const nodes = [];
1017
+ let node = walker.nextNode();
1018
+ while (node !== null) { nodes.push(node); node = walker.nextNode(); }
1019
+ for (const textNode of nodes) {
1020
+ const text = textNode.nodeValue ?? '';
1021
+ const lower = text.toLowerCase();
1022
+ const hits = [];
1023
+ let index = lower.indexOf(needle);
1024
+ while (index >= 0) { hits.push(index); index = lower.indexOf(needle, index + query.length); }
1025
+ for (let k = hits.length - 1; k >= 0; k -= 1) {
1039
1026
  try {
1040
- const parent = ranges[next].startContainer.parentElement;
1041
- if (parent && parent.scrollIntoView) parent.scrollIntoView({ block: 'center', behavior: 'smooth' });
1042
- } catch { /* scroll is best effort */ }
1043
- };
1044
- if (!open) {
1045
- return h('button', {
1046
- type: 'button',
1047
- className: 'w2mSearchToggle',
1048
- title: '搜索本对话',
1049
- 'aria-label': '搜索本对话',
1050
- onClick: () => setOpen(true),
1051
- }, '🔍');
1027
+ const range = document.createRange();
1028
+ range.setStart(textNode, hits[k]);
1029
+ range.setEnd(textNode, hits[k] + query.length);
1030
+ const mark = document.createElement('mark');
1031
+ mark.className = 'w2mSearchMark';
1032
+ mark.setAttribute('style', 'background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)');
1033
+ range.surroundContents(mark);
1034
+ total += 1;
1035
+ } catch { /* a range that crosses elements cannot be wrapped; skip it */ }
1052
1036
  }
1053
- return h('div', { className: 'w2mSearchBox', 'data-w2m-search': 'box' }, [
1054
- h('input', {
1055
- key: 'q',
1056
- ref: inputRef,
1057
- className: 'w2mSearchInput',
1058
- value: query,
1059
- placeholder: '搜索本对话全部内容',
1060
- onChange: (event) => setQuery(event.target.value),
1061
- onKeyDown: (event) => {
1062
- if (event.key === 'Enter') { event.preventDefault(); step(event.shiftKey); }
1063
- if (event.key === 'Escape') { setOpen(false); setQuery(''); }
1064
- },
1065
- }),
1066
- h('span', { key: 'count', className: 'w2mSearchCount' }, count === 0 ? (query === '' ? '' : '0') : whenCount(count, cursor)),
1067
- h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => step(true) }, '↑'),
1068
- h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => step(false) }, '↓'),
1069
- h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); clearSearchHighlight(); } }, '×'),
1070
- ]);
1071
1037
  }
1072
- function whenCount(total, at) {
1038
+ }
1039
+ return total;
1040
+ }
1041
+ function findScrollSurface() {
1042
+ if (typeof document === 'undefined') return null;
1043
+ let best = null;
1044
+ for (const root of searchRoots()) {
1045
+ let all = [];
1046
+ try { all = root.querySelectorAll ? [...root.querySelectorAll('*')] : []; } catch { continue; }
1047
+ for (const el of all) {
1048
+ if (el.scrollHeight <= el.clientHeight + 40) continue;
1049
+ const style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
1050
+ const overflow = style === null ? '' : style.overflowY;
1051
+ if (overflow !== 'auto' && overflow !== 'scroll') continue;
1052
+ if (best === null || el.scrollHeight > best.scrollHeight) best = el;
1053
+ }
1054
+ }
1055
+ return best;
1056
+ }
1057
+ function ConversationSearch() {
1058
+ const [open, setOpen] = React.useState(false);
1059
+ const [query, setQuery] = React.useState('');
1060
+ const [count, setCount] = React.useState(0);
1061
+ const [cursor, setCursor] = React.useState(-1);
1062
+ const inputRef = React.useRef(null);
1063
+ const holdRef = React.useRef(null);
1064
+ const longFiredRef = React.useRef(false);
1065
+ React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
1066
+ React.useEffect(() => {
1067
+ if (!open) { clearSearchMarks(); setCount(0); setCursor(-1); return undefined; }
1068
+ const total = paintSearchMarks(query);
1069
+ setCount(total);
1070
+ setCursor(total > 0 ? 0 : -1);
1071
+ const first = document.querySelector('mark.w2mSearchMark');
1072
+ if (first && first.scrollIntoView) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
1073
+ return () => clearSearchMarks();
1074
+ }, [open, query]);
1075
+ const step = (backwards) => {
1076
+ const marks = [...document.querySelectorAll('mark.w2mSearchMark')];
1077
+ if (marks.length === 0) { paintSearchMarks(query); return; }
1078
+ const next = (cursor + (backwards ? -1 : 1) + marks.length) % marks.length;
1079
+ setCursor(next);
1080
+ if (marks[next] && marks[next].scrollIntoView) marks[next].scrollIntoView({ block: 'center', behavior: 'smooth' });
1081
+ };
1082
+ const scrollTurns = () => {
1083
+ const surface = findScrollSurface();
1084
+ if (surface !== null) surface.scrollBy({ top: -Math.round(Math.max(240, surface.clientHeight * 1.8)), behavior: 'smooth' });
1085
+ else if (typeof window !== 'undefined') window.scrollBy({ top: -Math.round(window.innerHeight * 1.8), behavior: 'smooth' });
1086
+ };
1087
+ const scrollFirstTurn = () => {
1088
+ const surface = findScrollSurface();
1089
+ if (surface !== null) surface.scrollTo({ top: 0, behavior: 'smooth' });
1090
+ else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
1091
+ };
1092
+ if (!open) {
1093
+ return h('span', { className: 'w2mSearchControls' }, [
1094
+ h('button', {
1095
+ key: 'up',
1096
+ type: 'button',
1097
+ className: 'w2mScrollUp',
1098
+ title: '向上滚动 3 轮(长按 3 秒到第一轮)',
1099
+ 'aria-label': '向上滚动 3 轮,长按 3 秒到第一轮',
1100
+ onMouseDown: () => {
1101
+ longFiredRef.current = false;
1102
+ holdRef.current = setTimeout(() => { longFiredRef.current = true; scrollFirstTurn(); }, 3000);
1103
+ },
1104
+ onMouseUp: () => { if (holdRef.current) { clearTimeout(holdRef.current); holdRef.current = null; } },
1105
+ onMouseLeave: () => { if (holdRef.current) { clearTimeout(holdRef.current); holdRef.current = null; } },
1106
+ onClick: () => { if (longFiredRef.current) { longFiredRef.current = false; return; } scrollTurns(); },
1107
+ }, '⇧3'),
1108
+ h('button', {
1109
+ key: 'find',
1110
+ type: 'button',
1111
+ className: 'w2mSearchToggle',
1112
+ title: '搜索本对话',
1113
+ 'aria-label': '搜索本对话',
1114
+ onClick: () => setOpen(true),
1115
+ }, '🔍'),
1116
+ ]);
1117
+ }
1118
+ return h('div', { className: 'w2mSearchBox', 'data-w2m-search': 'box' }, [
1119
+ h('input', {
1120
+ key: 'q',
1121
+ ref: inputRef,
1122
+ className: 'w2mSearchInput',
1123
+ value: query,
1124
+ placeholder: '搜索本对话全部内容',
1125
+ onChange: (event) => setQuery(event.target.value),
1126
+ onKeyDown: (event) => {
1127
+ if (event.key === 'Enter') { event.preventDefault(); step(event.shiftKey); }
1128
+ if (event.key === 'Escape') { setOpen(false); setQuery(''); }
1129
+ },
1130
+ }),
1131
+ h('span', { key: 'count', className: 'w2mSearchCount' }, count === 0 ? (query === '' ? '' : '0') : whenCount(count, cursor)),
1132
+ h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => step(true) }, '↑'),
1133
+ h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => step(false) }, '↓'),
1134
+ h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); clearSearchMarks(); } }, '×'),
1135
+ ]);
1136
+ }
1137
+ function whenCount(total, at) {
1073
1138
  return (at >= 0 ? at + 1 : 0) + '/' + total;
1074
1139
  }
1075
1140
 
@@ -1667,7 +1732,22 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1667
1732
  }
1668
1733
 
1669
1734
  function FleetChat({ sessionId, viewMode }) {
1670
- const CHAT_EMOJIS = ['👍', '✅', '🚀', '😄', '❤️', '🎉', '🔧', '📌'];
1735
+ // v1.9.18: chat emotes are local slash commands, never image bytes. The relay only sees text.
1736
+ const CHAT_COMMANDS = ['/微笑', '/火箭', '/烟花'];
1737
+ // v1.9.18: the sticker pack is local to the plugin. Commands travel as text; rendering never requests bytes.
1738
+ const CHAT_STICKER_GLYPHS = { '/微笑': '\u{1F60A}', '/火箭': '\u{1F680}', '/烟花': '\u{1F386}' };
1739
+ const CHAT_STICKER_RE = /(\/微笑|\/火箭|\/烟花)/g;
1740
+ function renderChatText(text) {
1741
+ const raw = String(text ?? '');
1742
+ const whole = CHAT_STICKER_GLYPHS[raw.trim()];
1743
+ if (whole !== undefined) return h('span', { className: 'w2mChat_stickerBig', 'data-sticker': raw.trim(), title: raw.trim() }, whole);
1744
+ const parts = raw.split(CHAT_STICKER_RE);
1745
+ if (parts.length === 1) return raw;
1746
+ return parts.map((part, index) => (CHAT_STICKER_GLYPHS[part] !== undefined
1747
+ ? h('span', { key: 'st' + index, className: 'w2mChat_sticker', 'data-sticker': part, title: part }, CHAT_STICKER_GLYPHS[part])
1748
+ : part));
1749
+ }
1750
+ const CHAT_EMOJIS = CHAT_COMMANDS;
1671
1751
  const [open, setOpen] = React.useState(false);
1672
1752
  const [messages, setMessages] = React.useState([]);
1673
1753
  const [usage, setUsage] = React.useState(null);
@@ -1973,7 +2053,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
1973
2053
  var kind = messageKind(message, localMachineId);
1974
2054
  return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind, onClick: () => { const selection = typeof window.getSelection === 'function' ? String(window.getSelection() ?? '') : ''; if (selection !== '') return; setReplyTo({ id: message?.message_id ?? null, threadId: message?.thread_id ?? null, text: typeof message?.text === 'string' ? message.text : '' }); } },
1975
2055
  h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : '', h('button', { key: 'copy', type: 'button', className: 'w2mChat_copy', title: '复制这条消息', onClick: (event) => { event.stopPropagation(); copyText(String(message?.text ?? '')); } }, '⧉'), message?.thread_id ? h('span', { className: 'w2mChat_thread' }, '#' + message.thread_id) : null),
1976
- h('div', { className: 'w2mChat_text' }, message?.text ?? ''));
2056
+ h('div', { className: 'w2mChat_text' }, renderChatText(message?.text ?? '')));
1977
2057
  }),
1978
2058
  ].filter((node) => node !== null))),
1979
2059
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.9.17",
3
+ "version": "1.9.19",
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": {
@@ -64,7 +64,7 @@
64
64
  "pack": "node scripts/pack.mjs",
65
65
  "reachability": "node scripts/reachability-check.mjs",
66
66
  "reachability:classify": "node scripts/reachability-classify.mjs",
67
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs",
67
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs",
68
68
  "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
69
69
  "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
70
70
  "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
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": "1.9.17",
4
+ "version": "1.9.19",
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": [
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { Readable } from 'node:stream';
25
25
  import { access, constants as fsConstants } from 'node:fs/promises';
26
- import { existsSync, mkdirSync, mkdtempSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
26
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
27
27
  import { dirname, join } from 'node:path';
28
28
  import { commitTaskChanges, createWorktree, listChangedFiles, producePatch, removeWorktree } from './worktree.mjs';
29
29
  import { describePatchArtifact } from './patch-artifact.mjs';
@@ -31,6 +31,7 @@ import { ferryMaintenance, maintenancePaths } from './maintenance-ferry.mjs';
31
31
  import { authorizeWithGrant } from './grant-gate.mjs';
32
32
  import { tmpdir } from 'node:os';
33
33
  import { randomUUID , sign as signBytes } from 'node:crypto';
34
+ import { fileURLToPath } from 'node:url';
34
35
 
35
36
  import {
36
37
  EMPTY_SHA256,
@@ -2122,6 +2123,10 @@ export function createAgent(options) {
2122
2123
  python: caps.python ?? null,
2123
2124
  npm: caps.npm ?? null,
2124
2125
  git,
2126
+ plugin_version: (() => {
2127
+ try { return JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf8')).version ?? null; } catch { return null; }
2128
+ })(),
2129
+ joint_dev: 'v2',
2125
2130
  };
2126
2131
  return toolchainCache;
2127
2132
  }