@twinsearth/w2m-dsh-plugin 1.1.17 → 1.1.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.1.17"
8
+ "version": "1.1.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.1.17",
15
+ "version": "1.1.19",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -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.1.17",
4
+ "version": "1.1.19",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,81 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [1.1.19] — 2026-10-18
6
+
7
+ **The chat panel exists in the browser.**
8
+
9
+ Fleet chat shipped as a tool in 1.1.17 and as a relay API in the same release. What was missing was the half a user actually
10
+ touches: a control in the composer. This release adds it.
11
+
12
+ - **The panel**: a "fleet chat" control beside the target-machine control opens a floating panel listing this session's
13
+ messages, with the relay usage shown next to them, a multi-line input, and a send button. It refreshes while open.
14
+ - **The host route**: the browser never holds a device token. It calls `/api/w2m/chat` on the host, which proxies to the
15
+ relay's device-gated `/v1/fleet/messages` — so the token stays in the host process.
16
+ - **Tool bug fixed**: `w2m_chat` resolved its token through `readDevice(cfg)`, which fails when `stateDir` is unset even
17
+ though a `device.json` exists elsewhere; it now resolves exactly like every other tool (`resolveToken(cfg)`).
18
+ - **Theme discipline**: the panel carries no literal colours, only `--dsw-*` tokens, because a literal is wrong in one of
19
+ the two themes. The stylesheet test caught the one literal (`#fff` on the send button).
20
+
21
+ The relay on the fleet host was upgraded to this line of code in the same step: it had been running 1.0.14, which has no
22
+ `/v1/fleet/messages` route at all, so the panel would have shown "no route" forever. Verified after the restart: an
23
+ authenticated GET returns 200 (it returned 404 before), unauthenticated returns 401, and the fleet came back with the same
24
+ eight devices and no re-pairing.
25
+
26
+ ### Not in this release
27
+
28
+ The coloured per-machine avatars for chat messages are designed and not built. The panel shows one style for every
29
+ machine today.
30
+
31
+ ### Verified
32
+
33
+ Gate GREEN, 15 of 15. 2489 tests / 2471+ pass. client-bundle 25, tools 122, claims, portable-plugins 10.
34
+
35
+ ## [1.1.18] — 2026-10-18
36
+
37
+ **The developer declares this a STABLE release.** It ships the three-channel updater and the release plumbing that makes
38
+ the beta channel real.
39
+
40
+ ### Three channels, resolved from the two releases that exist
41
+
42
+ `w2m_update action:"status" channels:true` reports:
43
+
44
+ | Channel | Source | Rule |
45
+ |---|---|---|
46
+ | `stable` | GitHub's `/releases/latest` | never a prerelease |
47
+ | `beta` | the newest **prerelease**, fetched from `/releases` and filtered | exists only when one is published |
48
+ | `latest` | whichever of the two is newer by SemVer | equal versions resolve to stable |
49
+
50
+ Every decision comes from the existing `decideUpdate`, so the channel logic cannot drift from the install rule; this
51
+ module only chooses which two releases to ask about. A missing beta is reported as `available: null` rather than
52
+ substituted with stable — "there is no beta right now" is a fact the UI has to be able to show as a disabled button.
53
+
54
+ **The channel report is opt-in.** A plain `status` remains local and offline; making every status call reach GitHub would
55
+ slow the tool and break it behind a proxy. Asserted by a test that fails if any fetch happens.
56
+
57
+ ### Installing a channel, with the checksum enforced twice
58
+
59
+ `w2m_update action:"apply" channel:"stable"|"beta"|"latest"`: the release's `SHA256SUMS` is downloaded, the hash for the
60
+ tarball is required (no hash, no install), `downloadVerified` removes its temp file on any mismatch, and `installUpdate`
61
+ receives `expectedSha256` as well and keeps its rollback path. A mismatch is not retried: it means the asset and its
62
+ checksums disagree, which is never a transient condition.
63
+
64
+ ### The beta channel is a GitHub prerelease, and never npm
65
+
66
+ `.github/workflows/beta.yml`: a bump is a beta by default; a beta becomes a **prerelease** (so neither GitHub nor npm
67
+ resolves it as latest) gated by the `beta-access` environment, which requires a reviewer. There is **no `npm publish`
68
+ line in that file** — a channel cannot leak into the default install if the publication cannot happen at all. npm
69
+ receives a version only when the developer declares it stable, as this release does.
70
+
71
+ `.github/workflows/packages.yml` mirrors a released stable version to GitHub Packages with the registry overridden **on
72
+ the command line**, because `publishConfig.registry` would silently redirect the operator-channel publish too.
73
+
74
+ ### Verified
75
+
76
+ Gate GREEN twice, 15/15. 2504 tests / 2490 pass / 0 fail / 14 skip. tools 122, update-channels 4, update-prerelease 2.
77
+ The first GitHub Packages publication succeeded; the package page itself is confirmed by the operator, since the PAT has
78
+ no `read:packages` scope to read it back.
79
+
5
80
  ## [1.1.17] — 2026-10-18
6
81
 
7
82
  **Fleet chat: messages live in a session, are refused by a byte budget rather than silently dropped, are stored
package/README.md CHANGED
@@ -66,18 +66,18 @@ The manifests live in [`.claude-plugin/`](.claude-plugin/) and are version-locke
66
66
  # copy this block into your own codex/mcp-servers.toml
67
67
  [mcp_servers.w2m]
68
68
  command = "npx"
69
- args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
69
+ args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.19", "w2m-mcp"]
70
70
  ```
71
71
 
72
72
  ### 5. Verified release tarball (offline)
73
73
 
74
- From the `v1.1.17` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.17.tgz` and `SHA256SUMS` —
74
+ From the `v1.1.19` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.19.tgz` and `SHA256SUMS` —
75
75
  then:
76
76
 
77
77
  ```bash
78
- curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.17/twinsearth-w2m-dsh-plugin-1.1.17.tgz
78
+ curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.19/twinsearth-w2m-dsh-plugin-1.1.19.tgz
79
79
  sha256sum -c SHA256SUMS # verify before installing, always
80
- npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
80
+ npm install -g ./twinsearth-w2m-dsh-plugin-1.1.19.tgz
81
81
  ```
82
82
 
83
83
  Every release carries `SHA256SUMS` for exactly this reason. If a release is missing it, do not install that release.
@@ -198,6 +198,27 @@ Linux, npm + GitHub release + checksums consistent, end-to-end fleet runs on a r
198
198
 
199
199
  ---
200
200
 
201
+ ## Release channels
202
+
203
+ Three channels, and only one of them is what \
204
+ pm install\ gives you.
205
+
206
+ | Channel | Where it comes from | Who should use it |
207
+ |---|---|---|
208
+ | **stable** | npm \latest\ — a version the developer declared stable | everyone; this is the default install |
209
+ | **beta** | a GitHub **prerelease** (tarball + \SHA256SUMS\), **never on npm** | developers granted \eta-access\ who accept that it can fail |
210
+ | **latest build** | the newest of the two, whatever its channel | a developer who wants the newest code and knows the above |
211
+
212
+ A bump is a **beta by default**. npm receives a version only when the developer says, in so many words, that this one is a
213
+ stable release: a beta is published as a prerelease (so neither GitHub nor npm ever resolves it as \latest\), it is
214
+ downloadable only after the \eta-access\ environment approves the run, and installing it requires reading
215
+ [SECURITY.md](SECURITY.md) and accepting the risk.
216
+
217
+ There is no \
218
+ pm publish\ line in the beta workflow. That is deliberate — a channel cannot leak to the default install if
219
+ the publication cannot happen at all.
220
+ ---
221
+
201
222
  ## Links
202
223
 
203
224
  - [CHANGELOG](CHANGELOG.md) — every version, including what it deliberately did **not** ship
@@ -267,17 +288,17 @@ w2m-mcp --help
267
288
  # 把这段复制进你自己的 codex/mcp-servers.toml
268
289
  [mcp_servers.w2m]
269
290
  command = "npx"
270
- args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
291
+ args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.19", "w2m-mcp"]
271
292
  ```
272
293
 
273
294
  #### 5. 校验过的发布包(离线)
274
295
 
275
- 从 `v1.1.17` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.17.tgz` 与 `SHA256SUMS` —— 然后:
296
+ 从 `v1.1.19` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.19.tgz` 与 `SHA256SUMS` —— 然后:
276
297
 
277
298
  ```bash
278
- curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.17/twinsearth-w2m-dsh-plugin-1.1.17.tgz
299
+ curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.19/twinsearth-w2m-dsh-plugin-1.1.19.tgz
279
300
  sha256sum -c SHA256SUMS # 安装前务必校验
280
- npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
301
+ npm install -g ./twinsearth-w2m-dsh-plugin-1.1.19.tgz
281
302
  ```
282
303
 
283
304
  每个 release 都带 `SHA256SUMS` 正是为了这一步。若某个 release 缺少它,**不要安装该版本**。
@@ -372,6 +393,23 @@ CI 全绿、npm 与 GitHub 发布及校验和一致、在真实的 Linux/Mac/Win
372
393
  | 裁决总是 `partial` | 来自已下线机器的陈旧租约 | 1.1.2 已修:派发只对在线且未沉默的机器建租约 |
373
394
  | `P2P_PUNCH_FAILED` | NAT,或宿主密码学能力缺口 | 会自动降级到 relay 路径,结果不受影响 |
374
395
 
396
+ ### 发布通道(中文)
397
+
398
+ 三个通道,其中只有一个是你 \
399
+ pm install\ 默认拿到的。
400
+
401
+ | 通道 | 来源 | 适合谁 |
402
+ |---|---|---|
403
+ | **stable** | npm \latest\,开发者明确声明为稳定版 | 所有人;默认安装就是它 |
404
+ | **beta** | GitHub **prerelease**(tarball + \SHA256SUMS\),**绝不进 npm** | 被授予 \eta-access\、且接受它可能失败的开发者 |
405
+ | **最新构建** | 两者中版本较新的那个,不论通道 | 想要最新代码、并理解上述含义的开发者 |
406
+
407
+ **默认的 bump 是 beta**。只有开发者明确说明"这次是稳定版",npm 才会收到该版本:beta 以 prerelease 形式发布(因此
408
+ GitHub 与 npm 都不会把它解析成 \latest\),只有在 \eta-access\ 环境审批通过后才能下载,安装前需阅读
409
+ [SECURITY.md](SECURITY.md) 并自担风险。
410
+
411
+ beta 工作流里**没有 \
412
+ pm publish\ 这一行**,这是刻意的 —— 只要发布动作不可能发生,这个通道就不可能泄漏到默认安装。
375
413
  ### 链接
376
414
 
377
415
  - [CHANGELOG](CHANGELOG.md) —— 每个版本,包括它**故意没有**交付的内容
@@ -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.1.17", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.19", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
package/lib/client.js CHANGED
@@ -283,6 +283,24 @@ window.__ModuleLoader__.load({
283
283
  '.w2mTarget_name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
284
284
  '.w2mTarget_detail{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400}',
285
285
  '.w2mTarget_option:disabled .w2mTarget_detail{color:var(--dsw-alias-label-caption)}',
286
+ '.w2mChat{position:relative;display:inline-flex;align-items:center}',
287
+ '.w2mChat_trigger{display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 8px;border:none;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;line-height:20px;cursor:pointer;outline:none}',
288
+ '.w2mChat_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
289
+ '.w2mChat_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}',
290
+ '.w2mChat_panel{position:fixed;right:16px;bottom:88px;z-index:60;display:flex;flex-direction:column;width:340px;max-height:420px;border-radius:var(--dsw-radius-lg);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow:hidden}',
291
+ '.w2mChat_head{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-weight:600}',
292
+ '.w2mChat_usage{margin-left:auto;font-weight:400;color:var(--dsw-alias-label-tertiary);font-size:11px}',
293
+ '.w2mChat_close{border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:16px;line-height:16px;padding:0 2px}',
294
+ '.w2mChat_list{flex:1;overflow-y:auto;padding:8px 10px;display:flex;flex-direction:column;gap:8px;min-height:120px}',
295
+ '.w2mChat_empty{color:var(--dsw-alias-label-tertiary)}',
296
+ '.w2mChat_msg{display:flex;flex-direction:column;gap:2px}',
297
+ '.w2mChat_meta{color:var(--dsw-alias-label-tertiary);font-size:11px}',
298
+ '.w2mChat_text{white-space:pre-wrap;word-break:break-word}',
299
+ '.w2mChat_compose{display:flex;gap:8px;padding:8px 10px;border-top:.5px solid var(--dsw-alias-border-l2);align-items:flex-end}',
300
+ '.w2mChat_input{flex:1;resize:none;box-sizing:border-box;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);padding:6px 8px;outline:none;min-height:38px;max-height:96px}',
301
+ '.w2mChat_send{flex:none;height:28px;padding:0 10px;border:none;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-text-on-business);font:inherit;font-weight:500;cursor:pointer}',
302
+ '.w2mChat_send:disabled{opacity:.5;cursor:default}',
303
+ '.w2mChat_error{padding:0 10px 8px;color:var(--dsw-alias-state-error-primary);font-size:11px}',
286
304
  ].join('');
287
305
 
288
306
  /**
@@ -530,6 +548,148 @@ window.__ModuleLoader__.load({
530
548
  ]);
531
549
  }
532
550
 
551
+ // -------------------------------------------------------------------------------------------
552
+ // Fleet chat panel (v1.1.18 addition: the missing browser half of `w2m_chat`)
553
+ // -------------------------------------------------------------------------------------------
554
+ //
555
+ // v1.1.17 shipped fleet chat as a tool only — its own CHANGELOG records "no client UI panel
556
+ // beyond the generic tool card". This is that panel: a session-scoped broadcast board the
557
+ // operator and the agent share. Every read and write goes through the host route
558
+ // `/api/w2m/chat` (host-side `fleet-chat-route.mjs`), so the device token stays in the host
559
+ // process and this file never holds a credential.
560
+ const CHAT_ROUTE = '/api/w2m/chat';
561
+
562
+ /** One JSON call to the host chat route; a refusal is a value, not an exception. */
563
+ async function chatCall(options) {
564
+ let response;
565
+ try {
566
+ response = await fetch(options.url, options.init);
567
+ } catch (error) {
568
+ return { ok: false, error: `the plugin could not be reached: ${error instanceof Error ? error.message : String(error)}` };
569
+ }
570
+ let body = null;
571
+ try {
572
+ body = await response.json();
573
+ } catch {
574
+ return { ok: false, error: `the plugin answered HTTP ${response.status} with a body that is not JSON` };
575
+ }
576
+ if (response.status === 404) return { ok: false, error: 'this host does not serve the W2M chat route' };
577
+ if (response.ok !== true || body?.ok === false) {
578
+ return { ok: false, error: typeof body?.error === 'string' ? body.error : `HTTP ${response.status}` };
579
+ }
580
+ return { ok: true, state: body };
581
+ }
582
+
583
+ /**
584
+ * The fleet-chat control: opens a panel with the session's messages, a compose box and the
585
+ * relay's current usage. The text stays on the relay; this component keeps only the last view.
586
+ */
587
+ function FleetChat({ sessionId }) {
588
+ const [open, setOpen] = React.useState(false);
589
+ const [messages, setMessages] = React.useState([]);
590
+ const [usage, setUsage] = React.useState(null);
591
+ const [text, setText] = React.useState('');
592
+ const [error, setError] = React.useState(null);
593
+ const [busy, setBusy] = React.useState(false);
594
+
595
+ const load = React.useCallback(async (signal) => {
596
+ if (typeof sessionId !== 'string' || sessionId === '') return;
597
+ const result = await chatCall({
598
+ url: `${CHAT_ROUTE}?session_id=${encodeURIComponent(sessionId)}&limit=50`,
599
+ init: { method: 'GET', credentials: 'same-origin', headers: { accept: 'application/json' }, signal },
600
+ });
601
+ if (result.ok) {
602
+ setMessages(Array.isArray(result.state.messages) ? result.state.messages : []);
603
+ setUsage(result.state.usage ?? null);
604
+ setError(null);
605
+ } else {
606
+ setError(result.error);
607
+ }
608
+ }, [sessionId]);
609
+
610
+ React.useEffect(() => {
611
+ if (!open) return undefined;
612
+ const controller = new AbortController();
613
+ load(controller.signal);
614
+ const timer = setInterval(() => load(controller.signal), 3000);
615
+ return () => {
616
+ controller.abort();
617
+ clearInterval(timer);
618
+ };
619
+ }, [open, load]);
620
+
621
+ const send = React.useCallback(async () => {
622
+ const value = text.trim();
623
+ if (value === '' || busy) return undefined;
624
+ setBusy(true);
625
+ const result = await chatCall({
626
+ url: CHAT_ROUTE,
627
+ init: {
628
+ method: 'POST',
629
+ credentials: 'same-origin',
630
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
631
+ body: JSON.stringify({ session_id: sessionId, text: value }),
632
+ },
633
+ });
634
+ setBusy(false);
635
+ if (result.ok) {
636
+ setText('');
637
+ await load();
638
+ } else {
639
+ setError(result.error);
640
+ }
641
+ return undefined;
642
+ }, [busy, load, sessionId, text]);
643
+
644
+ const disabled = typeof sessionId !== 'string' || sessionId === '';
645
+ const usageText = usage === null ? null : `${Math.round((usage.used_24h_bytes ?? 0) / 1024)} KiB/24h · ${usage.messages ?? 0} 条`;
646
+ const machineLabel = (message) => {
647
+ const id = typeof message?.machine_id === 'string' ? message.machine_id : '?';
648
+ return id.length > 12 ? `${id.slice(0, 8)}…` : id;
649
+ };
650
+ const timeLabel = (message) => (Number.isFinite(message?.at_ms) ? new Date(message.at_ms).toLocaleTimeString() : '');
651
+
652
+ return h('span', { className: 'w2mChat' },
653
+ h('button', {
654
+ type: 'button',
655
+ className: 'w2mChat_trigger',
656
+ title: '编队聊天(本会话广播)',
657
+ 'aria-expanded': open,
658
+ disabled,
659
+ onClick: () => setOpen((value) => !value),
660
+ }, '💬', h('span', { className: 'w2mChat_label' }, '编队聊天')),
661
+ open
662
+ ? h('div', { className: 'w2mChat_panel', role: 'dialog', 'aria-label': '编队聊天' },
663
+ h('div', { className: 'w2mChat_head' },
664
+ h('span', null, '编队聊天'),
665
+ usageText === null ? null : h('span', { className: 'w2mChat_usage' }, usageText),
666
+ h('button', { type: 'button', className: 'w2mChat_close', title: '关闭', onClick: () => setOpen(false) }, '×')),
667
+ h('div', { className: 'w2mChat_list' },
668
+ error !== null ? h('div', { className: 'w2mChat_error' }, error) : null,
669
+ messages.length === 0
670
+ ? (error === null ? h('div', { className: 'w2mChat_empty' }, '本会话还没有消息') : null)
671
+ : messages.map((message) => h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg' },
672
+ h('div', { className: 'w2mChat_meta' }, `${machineLabel(message)} · ${timeLabel(message)}`),
673
+ h('div', { className: 'w2mChat_text' }, message?.text ?? '')))),
674
+ h('div', { className: 'w2mChat_compose' },
675
+ h('textarea', {
676
+ className: 'w2mChat_input',
677
+ value: text,
678
+ rows: 2,
679
+ placeholder: '说点什么,发给本会话的机器…',
680
+ disabled: busy,
681
+ onChange: (event) => setText(event.target.value),
682
+ onKeyDown: (event) => {
683
+ if (event.key === 'Enter' && event.shiftKey !== true) {
684
+ event.preventDefault();
685
+ send();
686
+ }
687
+ },
688
+ }),
689
+ h('button', { type: 'button', className: 'w2mChat_send', disabled: busy || text.trim() === '', onClick: send }, busy ? '发送中…' : '发送')))
690
+ : null);
691
+ }
692
+
533
693
  /**
534
694
  * Client plugin body: install the stylesheet, register the dictionaries, take the seat.
535
695
  *
@@ -607,6 +767,24 @@ window.__ModuleLoader__.load({
607
767
  t,
608
768
  }),
609
769
  }, TargetSelect));
770
+
771
+ // The fleet-chat panel: the same session-scoped seat, one slot after the target control.
772
+ // Wrapped in its own try/catch on purpose — the target control is the primary half of this
773
+ // bundle, and a chat panel that cannot mount must not take the page (or the app) down with it.
774
+ try {
775
+ slots.inject('conversation.input.left', () => slots.register({
776
+ name: 'conversation.input.left',
777
+ id: 'w2m-chat',
778
+ order: 41,
779
+ label: 'fleet chat',
780
+ inject: (sessionId) => ({ sessionId }),
781
+ }, FleetChat));
782
+ } catch (error) {
783
+ console.error(
784
+ '@twinsearth/w2m-dsh-plugin: the fleet chat panel was not mounted; the rest of the page is unaffected',
785
+ error,
786
+ );
787
+ }
610
788
  }
611
789
 
612
790
  /**
@@ -633,6 +811,7 @@ window.__ModuleLoader__.load({
633
811
  exports.apply = apply;
634
812
  exports.inject = inject;
635
813
  exports.TargetSelect = TargetSelect;
814
+ exports.FleetChat = FleetChat;
636
815
  exports.injectStyle = injectStyle;
637
816
  exports.makeTranslator = makeTranslator;
638
817
  return module.exports;