@twinsearth/w2m-dsh-plugin 1.9.8 → 1.9.9

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.8"
8
+ "version": "1.9.9"
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.8",
15
+ "version": "1.9.9",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -26,5 +26,5 @@
26
26
  ]
27
27
  }
28
28
  ],
29
- "version": "1.9.8"
29
+ "version": "1.9.9"
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.8",
4
+ "version": "1.9.9",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.9.9] - 2026-10-28
11
+
12
+ - 对话主工作区新增全文搜索:会话头部(置顶按钮同排)🔍 图标,点击展开搜索框,Enter/Shift+Enter 或 ↑/↓ 在已渲染对话中查找并高亮(Electron/Chromium find)。
13
+
14
+ - 修复 Windows「立即更新」失败:dsh 以 dsh.cmd 安装时,child_process.spawn(shell:false) 会报 EINVAL。现在 Windows 下不再把 .cmd/.bat 当作可直接执行计划,自动改用运行时 pnpm;若 dsh 计划出现 EINVAL,也会回退 pnpm 重试一次。
15
+ - 新增回归测试:.cmd 计划被拒绝并走 pnpm;spawn 失败仍回滚。
16
+
10
17
  ## [1.9.8] - 2026-10-28
11
18
 
12
19
  - 左栏 GPW 入口改为纯图标(🌐),去掉重复的文字条目。
@@ -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.8", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.9.9", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": "1.9.9",
3
+ "case": "RELEASE-199",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-27T22:54:14.249Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-199-1793141654250",
11
+ "requirement": "V1.9.9:修复 Windows 立即更新 spawn EINVAL(dsh.cmd),回退运行时 pnpm",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "plan": "win32 .cmd/.bat shim never selected",
16
+ "fallback": "EINVAL on dsh plan retries once with runtime pnpm",
17
+ "tests": "update-install 28/28 green",
18
+ "gates": "full suite + preflight expected GREEN before tag"
19
+ }
20
+ }
21
+ }
package/lib/client.js CHANGED
@@ -499,6 +499,12 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
499
499
  '.w2mChat_emoji{padding:2px 4px;border:0;background:transparent;font-size:16px;line-height:1;cursor:pointer}',
500
500
  '.w2mFleetStatus{display:flex;gap:16px;align-items:center;padding:2px 12px;font-size:11px;opacity:.8;white-space:nowrap}',
501
501
  '.w2mFleetStatusItem{white-space:nowrap}',
502
+ '.w2mSearchToggle{height:26px;width:26px;border:0;border-radius:6px;background:transparent;color:inherit;font-size:14px;cursor:pointer}',
503
+ '.w2mSearchToggle:hover{background:rgba(127,127,127,.14)}',
504
+ '.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
+ '.w2mSearchInput{height:24px;min-width:220px;border:0;background:transparent;color:inherit;font:inherit;outline:none}',
506
+ '.w2mSearchBtn{height:24px;min-width:24px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
507
+ '.w2mSearchBtn:hover{background:rgba(127,127,127,.14)}',
502
508
  '.w2mGpw_admin{padding:24px 32px 48px;box-sizing:border-box;max-width:1400px;margin:0 auto;color:var(--dsw-alias-label-primary);font-size:13px}',
503
509
  '.w2mGpw_adminHead{margin-bottom:16px}',
504
510
  '.w2mGpw_adminTitle{margin:0 0 6px;font-size:22px;line-height:30px;font-weight:600}',
@@ -794,7 +800,6 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
794
800
  },
795
801
  }, [
796
802
  h('span', { key: 'i', className: 'w2mGpw_sideIcon', 'aria-hidden': 'true' }, '🌐'),
797
- h('span', { key: 'l', className: 'w2mGpw_sideLabel' }, 'GPW'),
798
803
  ]);
799
804
  }
800
805
 
@@ -925,6 +930,52 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
925
930
  ]);
926
931
  }
927
932
 
933
+ /**
934
+ * v1.9.9: in-conversation full-text search. The icon sits in the session header next to Pin; the
935
+ * search itself uses the browser's own find (Electron/Chromium), which highlights and scrolls the
936
+ * rendered conversation without reading private session data through a plugin API.
937
+ */
938
+ function ConversationSearch() {
939
+ const [open, setOpen] = React.useState(false);
940
+ const [query, setQuery] = React.useState('');
941
+ const inputRef = React.useRef(null);
942
+ React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
943
+ const find = (backwards) => {
944
+ if (query === '') return;
945
+ try {
946
+ if (typeof window !== 'undefined' && typeof window.find === 'function') {
947
+ window.find(query, false, backwards === true, true);
948
+ }
949
+ } catch { /* find is best effort, never a crash */ }
950
+ };
951
+ if (!open) {
952
+ return h('button', {
953
+ type: 'button',
954
+ className: 'w2mSearchToggle',
955
+ title: '搜索本对话',
956
+ 'aria-label': '搜索本对话',
957
+ onClick: () => setOpen(true),
958
+ }, '🔍');
959
+ }
960
+ return h('div', { className: 'w2mSearchBox' }, [
961
+ h('input', {
962
+ key: 'q',
963
+ ref: inputRef,
964
+ className: 'w2mSearchInput',
965
+ value: query,
966
+ placeholder: '搜索本对话全部内容',
967
+ onChange: (event) => setQuery(event.target.value),
968
+ onKeyDown: (event) => {
969
+ if (event.key === 'Enter') { event.preventDefault(); find(event.shiftKey); }
970
+ if (event.key === 'Escape') { setOpen(false); setQuery(''); }
971
+ },
972
+ }),
973
+ h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => find(true) }, '↑'),
974
+ h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => find(false) }, '↓'),
975
+ h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); } }, '×'),
976
+ ]);
977
+ }
978
+
928
979
  /** v1.9.4: the GPW tab in Settings, between Models and Plugins. */
929
980
  function GpwSettingsPage({ t }) {
930
981
  return h('div', { className: 'w2mGpw_settingsBody' }, h(GpwPanel, { t, sessionId: undefined, inline: true, onClose: () => {} }));
@@ -2060,7 +2111,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
2060
2111
  name: 'sidebar.panellist',
2061
2112
  id: 'w2m-gpw',
2062
2113
  order: 4,
2063
- label: () => 'GPW',
2114
+ label: () => '',
2064
2115
  inject: () => ({}),
2065
2116
  }, GpwSidebarIcon));
2066
2117
  } catch (error) {
@@ -2128,6 +2179,18 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
2128
2179
 
2129
2180
 
2130
2181
 
2182
+ // v1.9.9: in-conversation search, in the session header next to Pin.
2183
+ try {
2184
+ slots.inject('conversation.session.header.actions', () => slots.register({
2185
+ name: 'conversation.session.header.actions',
2186
+ id: 'w2m-conversation-search',
2187
+ order: 90,
2188
+ label: () => '搜索本对话',
2189
+ inject: () => ({}),
2190
+ }, ConversationSearch));
2191
+ } catch (error) {
2192
+ console.error('@twinsearth/w2m-dsh-plugin: the conversation search control was not mounted', error);
2193
+ }
2131
2194
  // v1.9.6: fleet chat as a real conversation View tab (after Chat and Rules), rendering the
2132
2195
  // full chat page in the main workspace.
2133
2196
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.9.8",
3
+ "version": "1.9.9",
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": "1.9.8",
4
+ "version": "1.9.9",
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": [
@@ -283,7 +283,7 @@ const MAX_STAGES = 16;
283
283
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
284
284
  * overrides it at runtime for tests and packagers.
285
285
  */
286
- const PLUGIN_VERSION = '1.9.8';
286
+ const PLUGIN_VERSION = '1.9.9';
287
287
 
288
288
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
289
289
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -316,8 +316,12 @@ export function planInstall(input) {
316
316
  const dshPath = input.dshPath ?? null;
317
317
  const pnpmPath = input.pnpmPath ?? null;
318
318
  const nodePath = input.nodePath ?? process.execPath;
319
+ // v1.9.9: a .cmd/.bat shim is a Windows shell script; spawn(shell:false) refuses it with EINVAL
320
+ // ("could not run dsh (EINVAL: spawn EINVAL)"), and no other platform can execute it either.
321
+ // Only a directly executable path may take the dsh plan; anything else uses the runtime pnpm.
322
+ const dshSpawnable = dshPath !== null && !/\.(cmd|bat)$/i.test(dshPath);
319
323
 
320
- if (dshPath) {
324
+ if (dshSpawnable) {
321
325
  return {
322
326
  via: 'dsh',
323
327
  // `dsh plugin --profile <name> add <tarball>` is the supported surface;
@@ -531,7 +535,7 @@ export async function installUpdate(options = {}) {
531
535
  const packageName = manifest?.name ?? options.packageName ?? DEFAULT_PACKAGE_NAME;
532
536
  const from = readInstalledVersion(absoluteProfile, packageName);
533
537
 
534
- const plan = planInstall({
538
+ let plan = planInstall({
535
539
  profileDir: absoluteProfile,
536
540
  profileName,
537
541
  stagedTarball,
@@ -639,6 +643,21 @@ export async function installUpdate(options = {}) {
639
643
  };
640
644
  }
641
645
 
646
+ // v1.9.9: a spawn-level failure on the dsh plan (EINVAL/ENOENT) is retried once with the runtime
647
+ // pnpm, because that is the installer the same machine already uses successfully.
648
+ if (result?.spawnError?.code === 'EINVAL' && plan.via === 'dsh' && pnpmPath) {
649
+ const fallback = planInstall({ profileDir: absoluteProfile, profileName, stagedTarball, dshPath: null, pnpmPath, nodePath });
650
+ if (fallback !== null) {
651
+ step('dsh could not be spawned; retrying with the runtime pnpm');
652
+ plan = fallback;
653
+ try {
654
+ result = await runner({ argv: plan.argv, cwd: plan.cwd, via: plan.via });
655
+ } catch (error) {
656
+ result = { code: null, stdout: '', stderr: '', spawnError: { code: 'RUNNER_THREW', message: String(error?.message ?? error) }, timedOut: false };
657
+ }
658
+ }
659
+ }
660
+
642
661
  const stderrTail = tailBytes(result?.stderr ?? '', stderrTailBytes);
643
662
  const stdoutTail = tailBytes(result?.stdout ?? '', stderrTailBytes);
644
663
  const spawnFailed = result?.spawnError ?? null;