dsh-zen-remote 2.0.3 → 2.2.0

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.
Files changed (114) hide show
  1. package/AGENTS.md +15 -1
  2. package/README.md +11 -5
  3. package/README.zh-CN.md +11 -5
  4. package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +15 -0
  5. package/docs/adr/0002-relay-emulates-websocket.md +8 -0
  6. package/docs/interface.md +3 -1
  7. package/docs/plugin-bridge.md +140 -0
  8. package/docs/remote-access.en.md +57 -0
  9. package/docs/remote-access.md +57 -0
  10. package/lib/adapters/dsh-better-sidebar/index.js +144 -0
  11. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -0
  12. package/lib/adapters/dsh-better-sidebar/methods.js +198 -0
  13. package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -0
  14. package/lib/adapters/index.js +17 -0
  15. package/lib/adapters/index.js.map +1 -0
  16. package/lib/adapters/types.js +17 -0
  17. package/lib/adapters/types.js.map +1 -0
  18. package/lib/admin-routes.js +51 -0
  19. package/lib/admin-routes.js.map +1 -1
  20. package/lib/client-routes.js +18 -1
  21. package/lib/client-routes.js.map +1 -1
  22. package/lib/client.js +449 -45
  23. package/lib/config.js +27 -0
  24. package/lib/config.js.map +1 -1
  25. package/lib/http.js +10 -0
  26. package/lib/http.js.map +1 -1
  27. package/lib/index.js +246 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/plugin-adapter-core.js +385 -0
  30. package/lib/plugin-adapter-core.js.map +1 -0
  31. package/lib/plugin-bridge.js +318 -0
  32. package/lib/plugin-bridge.js.map +1 -0
  33. package/lib/plugin-loopback.js +207 -0
  34. package/lib/plugin-loopback.js.map +1 -0
  35. package/lib/plugin-route-intercept.js +1103 -0
  36. package/lib/plugin-route-intercept.js.map +1 -0
  37. package/lib/plugin-tunnel.js +416 -0
  38. package/lib/plugin-tunnel.js.map +1 -0
  39. package/lib/relay-client.js +319 -2
  40. package/lib/relay-client.js.map +1 -1
  41. package/lib/relay-server.js +633 -0
  42. package/lib/relay-server.js.map +1 -1
  43. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +27 -0
  44. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -0
  45. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts +51 -0
  46. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -0
  47. package/lib/types/adapters/index.d.ts +17 -0
  48. package/lib/types/adapters/index.d.ts.map +1 -0
  49. package/lib/types/adapters/types.d.ts +42 -0
  50. package/lib/types/adapters/types.d.ts.map +1 -0
  51. package/lib/types/admin-routes.d.ts +16 -0
  52. package/lib/types/admin-routes.d.ts.map +1 -1
  53. package/lib/types/client/effects/remote-session.d.ts +0 -22
  54. package/lib/types/client/effects/remote-session.d.ts.map +1 -1
  55. package/lib/types/client/locales.d.ts +16 -0
  56. package/lib/types/client/locales.d.ts.map +1 -1
  57. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  58. package/lib/types/client/styles/remote-session.css.d.ts +1 -1
  59. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
  60. package/lib/types/client-data/plugin-bridge.d.ts +79 -0
  61. package/lib/types/client-data/plugin-bridge.d.ts.map +1 -0
  62. package/lib/types/client-data/remote-status.d.ts +20 -0
  63. package/lib/types/client-data/remote-status.d.ts.map +1 -1
  64. package/lib/types/client-data/settings-form.d.ts +37 -1
  65. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  66. package/lib/types/client-routes.d.ts +13 -0
  67. package/lib/types/client-routes.d.ts.map +1 -1
  68. package/lib/types/config.d.ts +8 -0
  69. package/lib/types/config.d.ts.map +1 -1
  70. package/lib/types/http.d.ts +7 -0
  71. package/lib/types/http.d.ts.map +1 -1
  72. package/lib/types/index.d.ts +4 -0
  73. package/lib/types/index.d.ts.map +1 -1
  74. package/lib/types/plugin-adapter-core.d.ts +263 -0
  75. package/lib/types/plugin-adapter-core.d.ts.map +1 -0
  76. package/lib/types/plugin-bridge.d.ts +187 -0
  77. package/lib/types/plugin-bridge.d.ts.map +1 -0
  78. package/lib/types/plugin-loopback.d.ts +104 -0
  79. package/lib/types/plugin-loopback.d.ts.map +1 -0
  80. package/lib/types/plugin-route-intercept.d.ts +114 -0
  81. package/lib/types/plugin-route-intercept.d.ts.map +1 -0
  82. package/lib/types/plugin-tunnel.d.ts +96 -0
  83. package/lib/types/plugin-tunnel.d.ts.map +1 -0
  84. package/lib/types/relay-client.d.ts +76 -0
  85. package/lib/types/relay-client.d.ts.map +1 -1
  86. package/lib/types/relay-server.d.ts +32 -1
  87. package/lib/types/relay-server.d.ts.map +1 -1
  88. package/llms.txt +1 -0
  89. package/package.json +2 -2
  90. package/scripts/check-plugin-bridge-ui.mjs +168 -0
  91. package/scripts/check-settings-form.mjs +3 -1
  92. package/src/adapters/dsh-better-sidebar/index.ts +144 -0
  93. package/src/adapters/dsh-better-sidebar/methods.ts +215 -0
  94. package/src/adapters/index.ts +19 -0
  95. package/src/adapters/types.ts +44 -0
  96. package/src/admin-routes.ts +64 -0
  97. package/src/client/effects/remote-session.ts +82 -10
  98. package/src/client/locales.ts +36 -0
  99. package/src/client/settings/SettingsSection.tsx +107 -3
  100. package/src/client/styles/remote-session.css.ts +70 -31
  101. package/src/client-data/plugin-bridge.ts +150 -0
  102. package/src/client-data/remote-status.ts +30 -0
  103. package/src/client-data/settings-form.ts +78 -0
  104. package/src/client-routes.ts +29 -1
  105. package/src/config.ts +32 -0
  106. package/src/http.ts +12 -0
  107. package/src/index.ts +252 -0
  108. package/src/plugin-adapter-core.ts +530 -0
  109. package/src/plugin-bridge.ts +435 -0
  110. package/src/plugin-loopback.ts +299 -0
  111. package/src/plugin-route-intercept.ts +1193 -0
  112. package/src/plugin-tunnel.ts +470 -0
  113. package/src/relay-client.ts +394 -2
  114. package/src/relay-server.ts +656 -1
package/lib/client.js CHANGED
@@ -1850,6 +1850,123 @@ exports.RESTART_FIELDS = [
1850
1850
  'pushTool',
1851
1851
  ];
1852
1852
  };
1853
+ __modules["client-data/plugin-bridge.js"] = function (require, module, exports) {
1854
+ "use strict";
1855
+ /**
1856
+ * The plugin bridge's settings-page pure pieces (PB-5): the status → copy
1857
+ * mapping the 子客户端诊断区 renders, and the consent-toggle arithmetic the
1858
+ * 主服务端插件桥 section stages. Pure by the settings-form discipline — no
1859
+ * fetch, no clock, no context — so scripts/check-plugin-bridge-ui.mjs pins
1860
+ * every copy and every toggle shape.
1861
+ *
1862
+ * Deliberately import-free: this file rides the BROWSER bundle, and the
1863
+ * bridge's own module (src/plugin-bridge.ts) drags node:crypto in. The
1864
+ * status shape is therefore restated structurally here — it mirrors
1865
+ * lib/plugin-bridge.js's `BridgeStatus` verbatim, and a drifted wire is
1866
+ * handled by the tolerant mapping, not by the type system.
1867
+ */
1868
+ Object.defineProperty(exports, "__esModule", { value: true });
1869
+ exports.derivePluginBridgeStatuses = derivePluginBridgeStatuses;
1870
+ exports.bridgeSectionState = bridgeSectionState;
1871
+ exports.togglePluginConsent = togglePluginConsent;
1872
+ exports.readAdminBridgeRow = readAdminBridgeRow;
1873
+ /** The reason → copy-key table, one entry per BridgeReason plus the
1874
+ * everything-else fallback (an unknown reason from a newer server reads as
1875
+ * 未桥接-generic — the offline copy, honest without inventing a claim). */
1876
+ const REASON_KEYS = {
1877
+ 'not-on-server': 'settings.bridge.stateNotOnServer',
1878
+ 'version-mismatch': 'settings.bridge.stateVersionMismatch',
1879
+ 'declaration-mismatch': 'settings.bridge.stateDeclarationMismatch',
1880
+ 'consent-off': 'settings.bridge.stateConsentOff',
1881
+ offline: 'settings.bridge.stateOffline',
1882
+ 'server-too-old': 'settings.bridge.stateServerTooOld',
1883
+ };
1884
+ /** Tolerant read of one `pluginBridge` wire array into the render rows,
1885
+ * input order kept (the route already sorts by plugin name). A malformed
1886
+ * entry is skipped, never thrown into the settings page. */
1887
+ function derivePluginBridgeStatuses(value) {
1888
+ if (!Array.isArray(value))
1889
+ return [];
1890
+ const rows = [];
1891
+ for (const entry of value) {
1892
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
1893
+ continue;
1894
+ const record = entry;
1895
+ if (typeof record.plugin !== 'string' || record.plugin === '')
1896
+ continue;
1897
+ const bridged = record.bridged === true;
1898
+ const reason = typeof record.reason === 'string' ? record.reason : undefined;
1899
+ rows.push({
1900
+ plugin: record.plugin,
1901
+ localVersion: typeof record.localVersion === 'string' ? record.localVersion : '',
1902
+ stateKey: bridged
1903
+ ? 'settings.bridge.stateBridged'
1904
+ : reason !== undefined && REASON_KEYS[reason] !== undefined
1905
+ ? REASON_KEYS[reason]
1906
+ : 'settings.bridge.stateOffline',
1907
+ serverVersion: typeof record.serverVersion === 'string' ? record.serverVersion : undefined,
1908
+ });
1909
+ }
1910
+ return rows;
1911
+ }
1912
+ /**
1913
+ * The three-state resolution for the host settings page's 插件桥 list
1914
+ * (PB-9-fix): a fetch that has not answered is LOADING, a failed or
1915
+ * refused fetch is an ERROR — and only a successful answer with zero rows
1916
+ * means "no plugin has registered a bridge". Collapsing the first two into
1917
+ * the empty copy would tell the user nothing is bridged while the answer is
1918
+ * simply not in yet (or the page was opened through the gateway, where the
1919
+ * listing is refused). Pure so the check pins all four outcomes.
1920
+ */
1921
+ function bridgeSectionState(load, rows) {
1922
+ if (load === 'loading')
1923
+ return 'loading';
1924
+ if (load === 'error')
1925
+ return 'error';
1926
+ return Array.isArray(rows) && rows.length > 0 ? 'plugins' : 'empty';
1927
+ }
1928
+ /**
1929
+ * One consent-toggle click: the NEXT whole-machine-data consent map, staged
1930
+ * over the row's `pluginBridgeConsent` field. Only `plugin`'s key moves —
1931
+ * every other key (including entries for plugins no longer registered; the
1932
+ * settings page deliberately never prunes what it does not render) is
1933
+ * carried over verbatim, in its existing key order. An illegal current map
1934
+ * (a hand-edited row) reads as the empty map: the toggle then writes exactly
1935
+ * one key, and the junk is left for the row validator to keep refusing.
1936
+ */
1937
+ function togglePluginConsent(current, plugin, allow) {
1938
+ const next = {};
1939
+ if (current !== null && typeof current === 'object' && !Array.isArray(current)) {
1940
+ for (const [key, value] of Object.entries(current)) {
1941
+ if (typeof value === 'boolean')
1942
+ next[key] = value;
1943
+ }
1944
+ }
1945
+ next[plugin] = allow;
1946
+ return next;
1947
+ }
1948
+ /**
1949
+ * Tolerant read of one admin listing row into the render shape (PB-11).
1950
+ * A malformed row is `undefined`, never a throw — the settings page skips
1951
+ * it the same way {@link derivePluginBridgeStatuses} skips a bad verdict.
1952
+ * `adapted` defaults to false: an older server's answer simply reads as
1953
+ * "nothing is adapted".
1954
+ */
1955
+ function readAdminBridgeRow(row) {
1956
+ if (row === null || typeof row !== 'object' || Array.isArray(row))
1957
+ return undefined;
1958
+ const record = row;
1959
+ if (typeof record.plugin !== 'string' || record.plugin === '')
1960
+ return undefined;
1961
+ return {
1962
+ plugin: record.plugin,
1963
+ version: typeof record.version === 'string' ? record.version : '',
1964
+ hasServerScope: record.hasServerScope === true,
1965
+ consent: record.consent === true,
1966
+ adapted: record.adapted === true,
1967
+ };
1968
+ }
1969
+ };
1853
1970
  __modules["client-data/settings-form.js"] = function (require, module, exports) {
1854
1971
  "use strict";
1855
1972
  /**
@@ -1879,7 +1996,7 @@ __modules["client-data/settings-form.js"] = function (require, module, exports)
1879
1996
  * (devices / pairing) plus the T14 envelope (ok / gateway / config / viaGateway).
1880
1997
  */
1881
1998
  Object.defineProperty(exports, "__esModule", { value: true });
1882
- exports.ZenRemoteSettingsForm = exports.SETTINGS_FIELDS = exports.IDLE_HOURS_MAX = exports.SERVER_NAME_MAX = exports.RESTART_FIELDS = exports.DEVICE_TOKEN_FIELD = exports.CLIENT_CONFIG_ROUTE = exports.CLIENT_RECONNECT_ROUTE = exports.CLIENT_STATUS_ROUTE = exports.CLIENT_CLAIM_ROUTE = exports.ADMIN_PUSH_TEST_ROUTE = exports.ADMIN_ACTION_ROUTE = exports.ADMIN_PAIR_ROUTE = exports.ADMIN_STATUS_ROUTE = void 0;
1999
+ exports.ZenRemoteSettingsForm = exports.SETTINGS_FIELDS = exports.IDLE_HOURS_MAX = exports.SERVER_NAME_MAX = exports.RESTART_FIELDS = exports.DEVICE_TOKEN_FIELD = exports.CLIENT_CONFIG_ROUTE = exports.CLIENT_RECONNECT_ROUTE = exports.CLIENT_STATUS_ROUTE = exports.CLIENT_CLAIM_ROUTE = exports.ADMIN_PLUGIN_BRIDGE_ROUTE = exports.ADMIN_PUSH_TEST_ROUTE = exports.ADMIN_ACTION_ROUTE = exports.ADMIN_PAIR_ROUTE = exports.ADMIN_STATUS_ROUTE = void 0;
1883
2000
  exports.deriveClientStatusView = deriveClientStatusView;
1884
2001
  exports.clientStatusLineOf = clientStatusLineOf;
1885
2002
  exports.normalizePairingCode = normalizePairingCode;
@@ -1895,14 +2012,18 @@ exports.nameField = nameField;
1895
2012
  exports.claimDeviceNameOf = claimDeviceNameOf;
1896
2013
  exports.oneOfField = oneOfField;
1897
2014
  exports.boolField = boolField;
2015
+ exports.consentField = consentField;
1898
2016
  exports.localOpsAllowed = localOpsAllowed;
1899
2017
  exports.deriveSettingsView = deriveSettingsView;
1900
2018
  const restart_fields_ts_1 = require("./restart-fields.js");
2019
+ const plugin_bridge_ts_1 = require("./client-data/plugin-bridge.js");
1901
2020
  /** Same-origin admin routes the settings block talks to (host half: T14). */
1902
2021
  exports.ADMIN_STATUS_ROUTE = '/_dsh/zen-remote/admin/status';
1903
2022
  exports.ADMIN_PAIR_ROUTE = '/_dsh/zen-remote/admin/pair';
1904
2023
  exports.ADMIN_ACTION_ROUTE = '/_dsh/zen-remote/admin/action';
1905
2024
  exports.ADMIN_PUSH_TEST_ROUTE = '/_dsh/zen-remote/admin/push-test';
2025
+ /** PB-5: the plugin bridge's registered-plugin listing (GET, server-local only). */
2026
+ exports.ADMIN_PLUGIN_BRIDGE_ROUTE = '/_dsh/zen-remote/admin/plugin-bridge';
1906
2027
  /** Same-origin client routes the sub-client block talks to (host half: T16). */
1907
2028
  exports.CLIENT_CLAIM_ROUTE = '/_dsh/zen-remote/client/claim';
1908
2029
  exports.CLIENT_STATUS_ROUTE = '/_dsh/zen-remote/client/status';
@@ -1972,6 +2093,18 @@ function deriveInterceptView(value) {
1972
2093
  recentFailures: failureListOf(record.recentFailures),
1973
2094
  };
1974
2095
  }
2096
+ /** PB-4 route-interception block, presence-gated the same way: only the
2097
+ * install flag and the shape reasons (the 本机改道未启用 line) — the
2098
+ * counters belong to the status surface, not this copy. */
2099
+ function derivePluginRouteInterceptView(value) {
2100
+ if (value === undefined || value === null)
2101
+ return undefined;
2102
+ const record = asRecord(value);
2103
+ return {
2104
+ installed: record.installed === true,
2105
+ reasons: stringListOf(record.reasons, 20),
2106
+ };
2107
+ }
1975
2108
  /** T42 compat block, only when the body carried the field at all. The wire
1976
2109
  * names are the status route's (`different` / `incompatibleCalls`); the view
1977
2110
  * keeps the render-side names the block was written against. */
@@ -2016,6 +2149,8 @@ function deriveClientStatusView(body) {
2016
2149
  deviceName: typeof safe.deviceName === 'string' ? safe.deviceName : '',
2017
2150
  intercept: deriveInterceptView(safe.intercept),
2018
2151
  compat: deriveCompatView(safe.compat),
2152
+ pluginBridge: (0, plugin_bridge_ts_1.derivePluginBridgeStatuses)(safe.pluginBridge),
2153
+ pluginRouteIntercept: derivePluginRouteInterceptView(safe.pluginRouteIntercept),
2019
2154
  };
2020
2155
  }
2021
2156
  function clientStatusLineOf(view, now) {
@@ -2215,6 +2350,51 @@ function boolField(field) {
2215
2350
  parse: (text) => (text === 'true' || text === 'false' ? { kind: 'set', value: text === 'true' } : undefined),
2216
2351
  };
2217
2352
  }
2353
+ /**
2354
+ * `pluginBridgeConsent` (PB-5): a name → boolean map staged as its canonical
2355
+ * JSON draft. Nothing renders it as text — the 插件桥 toggles stage the NEXT
2356
+ * map wholesale (`togglePluginConsent` builds it, one key per click) — but
2357
+ * riding the shared spec table is what puts the field on the SAME save path
2358
+ * as `autoShareNewSessions`: one staged revision, one save button, one
2359
+ * atomic mutation. `format` keeps the value's own key order so a staged
2360
+ * draft equal to the displayed value reads as "no change"; an illegal stored
2361
+ * map formats as `{}` (the toggle then writes from scratch, and the junk
2362
+ * stays refused by the row validator, never laundered through).
2363
+ */
2364
+ function consentField(field) {
2365
+ const format = (value) => {
2366
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
2367
+ return '{}';
2368
+ const out = {};
2369
+ for (const [key, entry] of Object.entries(value)) {
2370
+ if (typeof entry === 'boolean')
2371
+ out[key] = entry;
2372
+ }
2373
+ return JSON.stringify(out);
2374
+ };
2375
+ return {
2376
+ field,
2377
+ format,
2378
+ parse: (text) => {
2379
+ let parsed;
2380
+ try {
2381
+ parsed = JSON.parse(text);
2382
+ }
2383
+ catch {
2384
+ return undefined;
2385
+ }
2386
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
2387
+ return undefined;
2388
+ const out = {};
2389
+ for (const [key, entry] of Object.entries(parsed)) {
2390
+ if (typeof entry !== 'boolean')
2391
+ return undefined;
2392
+ out[key] = entry;
2393
+ }
2394
+ return { kind: 'set', value: out };
2395
+ },
2396
+ };
2397
+ }
2218
2398
  /** Longest legal `serverName`, mirroring src/config.ts's cap. */
2219
2399
  exports.SERVER_NAME_MAX = 40;
2220
2400
  /** Upper bound of `idleHours`, mirroring src/config.ts. */
@@ -2235,6 +2415,7 @@ exports.SETTINGS_FIELDS = [
2235
2415
  nameField('serverName', exports.SERVER_NAME_MAX),
2236
2416
  hoursField('idleHours', exports.IDLE_HOURS_MAX),
2237
2417
  boolField('autoShareNewSessions'),
2418
+ consentField('pluginBridgeConsent'),
2238
2419
  ];
2239
2420
  // --- the status -> view mapping ---------------------------------------------
2240
2421
  /**
@@ -3038,6 +3219,22 @@ exports.zh = {
3038
3219
  'settings.client.diagRecentFailures': '最近的远程调用失败',
3039
3220
  'settings.client.diagCompatGroups': '指纹有差异的组:{groups}',
3040
3221
  'settings.client.diagCompatCalls': '最近的不兼容调用',
3222
+ // --- PB-5 插件桥:主服务端同意小节 + 子客户端诊断小节 ---
3223
+ 'settings.bridge.title': '插件桥',
3224
+ 'settings.bridge.empty': '暂无插件登记插件桥',
3225
+ 'settings.bridge.loading': '正在读取插件桥登记…',
3226
+ 'settings.bridge.loadFailed': '插件桥列表读取失败(旧版服务端或经网关打开的页面)',
3227
+ 'settings.bridge.sessionOnly': '随会话共享转发',
3228
+ 'settings.bridge.consentToggle': '允许转发整机数据',
3229
+ 'settings.bridge.consentHint': '打开后,已配对的桌面应用端可在远程会话里读取该插件的整机数据。',
3230
+ 'settings.bridge.stateBridged': '已桥接',
3231
+ 'settings.bridge.stateNotOnServer': '服务端未安装',
3232
+ 'settings.bridge.stateVersionMismatch': '版本不同(服务端 {version})',
3233
+ 'settings.bridge.stateDeclarationMismatch': '声明不一致',
3234
+ 'settings.bridge.stateConsentOff': '服务端主人未允许整机数据',
3235
+ 'settings.bridge.stateOffline': '中继离线',
3236
+ 'settings.bridge.stateServerTooOld': '服务端 zen-remote 版本过旧',
3237
+ 'settings.bridge.interceptOff': '本机改道未启用:{reason}',
3041
3238
  // --- T34 子客户端远程状态:标题行图标、横幅、关闭远程 ---
3042
3239
  'remoteStatusOnline': '远程会话 · 已连接',
3043
3240
  'remoteStatusOffline': '服务端离线,正在重连',
@@ -3054,6 +3251,9 @@ exports.zh = {
3054
3251
  'remoteStatusOfflineClick': '服务端离线,无法操作',
3055
3252
  'remoteStatusUnshareFail': '关闭远程失败,请稍后再试',
3056
3253
  'remoteStatusUnshareFailCode': '关闭远程失败({code}),请稍后再试',
3254
+ // --- PB-14: the unbridged better-sidebar note (a CSS variable the
3255
+ // remote-session stylesheet spends as the empty tab's ::before copy) ---
3256
+ 'remoteBridgeUnbridgedNote': 'better-sidebar 未在远程会话桥接(服务端未装同版本或未允许),文件与改动请以会话内消息为准',
3057
3257
  // 远程分组标题的后缀(后台 merge-streams 拿不到界面语言,固定用中文);
3058
3258
  // 这里登记英文对照,供界面侧需要复述同样状态时使用。
3059
3259
  'remoteGroupOffline': '(离线)',
@@ -3308,6 +3508,22 @@ exports.en = {
3308
3508
  'settings.client.diagRecentFailures': 'Recent remote call failures',
3309
3509
  'settings.client.diagCompatGroups': 'Groups with fingerprint differences: {groups}',
3310
3510
  'settings.client.diagCompatCalls': 'Recent incompatible calls',
3511
+ // --- PB-5 plugin bridge: host consent section + sub-client diagnostics ---
3512
+ 'settings.bridge.title': 'Plugin bridge',
3513
+ 'settings.bridge.empty': 'No plugin has registered a bridge yet',
3514
+ 'settings.bridge.loading': 'Loading the bridge registrations…',
3515
+ 'settings.bridge.loadFailed': 'The bridge listing failed to load (an older server, or the page opened through the gateway)',
3516
+ 'settings.bridge.sessionOnly': 'Forwards with shared sessions',
3517
+ 'settings.bridge.consentToggle': 'Allow forwarding whole-machine data',
3518
+ 'settings.bridge.consentHint': 'When on, paired desktop apps can read this plugin\u2019s whole-machine data in remote sessions.',
3519
+ 'settings.bridge.stateBridged': 'Bridged',
3520
+ 'settings.bridge.stateNotOnServer': 'Not installed on the server',
3521
+ 'settings.bridge.stateVersionMismatch': 'Version differs (server {version})',
3522
+ 'settings.bridge.stateDeclarationMismatch': 'Declaration mismatch',
3523
+ 'settings.bridge.stateConsentOff': 'Server owner has not allowed whole-machine data',
3524
+ 'settings.bridge.stateOffline': 'Relay offline',
3525
+ 'settings.bridge.stateServerTooOld': 'Server zen-remote is too old',
3526
+ 'settings.bridge.interceptOff': 'Local rerouting not enabled: {reason}',
3311
3527
  // --- T34 remote-status icon, composer banner, unshare ---
3312
3528
  'remoteStatusOnline': 'Remote session · connected',
3313
3529
  'remoteStatusOffline': 'Server offline — reconnecting',
@@ -3324,6 +3540,7 @@ exports.en = {
3324
3540
  'remoteStatusOfflineClick': 'The server is offline — nothing to do',
3325
3541
  'remoteStatusUnshareFail': 'Closing the remote failed — try again later',
3326
3542
  'remoteStatusUnshareFailCode': 'Closing the remote failed ({code}) — try again later',
3543
+ 'remoteBridgeUnbridgedNote': 'better-sidebar is not bridged for this remote session (the server lacks the same version or has not allowed it) — treat files and changes as session messages only',
3327
3544
  // Remote group title suffixes (the background merger cannot know the UI
3328
3545
  // language and always uses the Chinese forms); the English equivalents
3329
3546
  // live here for UI surfaces that restate the same status.
@@ -15067,6 +15284,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
15067
15284
  const react_1 = require("react");
15068
15285
  const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
15069
15286
  const settings_form_ts_1 = require("./client-data/settings-form.js");
15287
+ const plugin_bridge_ts_1 = require("./client-data/plugin-bridge.js");
15070
15288
  const shares_ts_1 = require("./client-data/shares.js");
15071
15289
  /** Device roles the pairing picker offers, in display order. */
15072
15290
  const PAIR_ROLES = ['web', 'desktop-client'];
@@ -15320,6 +15538,41 @@ function SettingsSectionPage({ config, shares, t }) {
15320
15538
  ? (0, settings_form_ts_1.deriveSettingsView)(load.body, { now, rowUser: config.rowUser() })
15321
15539
  : undefined;
15322
15540
  const clientRole = config.savedRoleIsClient();
15541
+ // The plugin bridge's registered-plugin listing (PB-5), host page only:
15542
+ // loaded once per page life and refreshed whenever a save lands (the
15543
+ // consent staged by the toggles below rides that very save). Starts in
15544
+ // the loading state — the section renders the loading copy until the
15545
+ // first answer lands (or the load fails, which is its own state, never
15546
+ // the empty one); a composition without the route stays loading.
15547
+ // PB-9-fix: the listing is a THREE-state read — loading, failed (an old
15548
+ // build, or the page opened through the gateway where the listing is
15549
+ // refused), or an answer that carries zero rows. Only the third means
15550
+ // "nothing registered"; the first two must not masquerade as it.
15551
+ const [bridgeLoad, setBridgeLoad] = (0, react_1.useState)({ state: 'loading', rows: [] });
15552
+ const loadBridge = (0, react_1.useCallback)(() => {
15553
+ setBridgeLoad((prev) => ({ state: 'loading', rows: prev.rows }));
15554
+ fetch(settings_form_ts_1.ADMIN_PLUGIN_BRIDGE_ROUTE)
15555
+ .then((res) => {
15556
+ if (!res.ok)
15557
+ return Promise.reject(new Error(String(res.status)));
15558
+ return res.json();
15559
+ })
15560
+ .then((body) => {
15561
+ const plugins = body?.value?.plugins;
15562
+ if (!Array.isArray(plugins))
15563
+ return;
15564
+ setBridgeLoad({
15565
+ state: 'ready',
15566
+ rows: plugins.filter((row) => row !== null && typeof row === 'object' && typeof row.plugin === 'string'),
15567
+ });
15568
+ })
15569
+ .catch(() => { setBridgeLoad((prev) => ({ state: 'error', rows: prev.rows })); });
15570
+ }, []);
15571
+ (0, react_1.useEffect)(() => {
15572
+ if (!clientRole)
15573
+ loadBridge();
15574
+ }, [clientRole, loadBridge]);
15575
+ const bridgeState = (0, plugin_bridge_ts_1.bridgeSectionState)(bridgeLoad.state, bridgeLoad.rows);
15323
15576
  const statusPoll = config.statusPoll();
15324
15577
  (0, react_1.useEffect)(() => {
15325
15578
  if (statusPoll === 'client')
@@ -15632,6 +15885,10 @@ function SettingsSectionPage({ config, shares, t }) {
15632
15885
  setClientLoad({ state: 'loading' });
15633
15886
  const refresh = config.savedRoleIsClient() ? loadClientStatus : loadStatus;
15634
15887
  refresh();
15888
+ // The plugin bridge listing re-reads too (PB-5): the consent toggles
15889
+ // stage through the very save that just landed.
15890
+ if (!config.savedRoleIsClient())
15891
+ loadBridge();
15635
15892
  // Immediate pull + 1.5s covers a hot-only save. A save that reloads the
15636
15893
  // plugin row (T17) restarts the row and the gateway child with it — the
15637
15894
  // reload and the gateway's rebinding of its port both take longer than
@@ -15653,7 +15910,19 @@ function SettingsSectionPage({ config, shares, t }) {
15653
15910
  const reloadsRow = saveReloadsRow();
15654
15911
  void config.save().then((landed) => { if (landed)
15655
15912
  saveLanded(reloadsRow); });
15656
- }, onDiscard: () => { config.discard(); }, children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.roleTitle') }), select(form.role, 'role', t('settings.role'), t('settings.roleHint'), ['host', 'client'], (option) => (option === 'host' ? t('settings.roleHost') : t('settings.roleClient'))), text(form.serverName, 'serverName', t('settings.fieldServerName'), t(clientRole ? 'settings.fieldServerNameHintClient' : 'settings.fieldServerNameHint'), t('settings.invalidName')), reloadPending && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", children: t('settings.reloadNote') }), !clientRole && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.gatewayTitle') }), text(form.host, 'host', t('settings.fieldHost'), t('settings.fieldHostHint'), '', false, t('settings.fieldHostNote')), text(form.port, 'port', t('settings.fieldPort'), t('settings.fieldPortHint'), t('settings.invalidPort'), true), text(form.trustedProxies, 'trustedProxies', t('settings.fieldTrustedProxies'), t('settings.fieldTrustedProxiesHint'), ''), text(form.rateLimit, 'rateLimit', t('settings.fieldRateLimit'), t('settings.fieldRateLimitHint'), t('settings.invalidPositiveInt'), true), text(form.vapidSubject, 'vapidSubject', t('settings.fieldVapidSubject'), t('settings.fieldVapidSubjectHint'), ''), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.pushTitle') }), bool(form.pushSummary, 'pushSummary', t('settings.fieldPushSummary'), t('settings.fieldPushSummaryHint')), bool(form.pushTurnEnd, 'pushTurnEnd', t('settings.fieldPushTurnEnd'), t('settings.fieldPushTurnEndHint')), bool(form.pushTool, 'pushTool', t('settings.fieldPushTool'), t('settings.fieldPushToolHint')), text(form.pushDebounceMs, 'pushDebounceMs', t('settings.fieldPushDebounceMs'), t('settings.fieldPushDebounceMsHint'), t('settings.invalidNonNegativeInt'), true), select(form.lang, 'lang', t('settings.fieldLang'), t('settings.fieldLangHint'), ['auto', 'zh', 'en'], (option) => (option === 'auto' ? t('settings.langAuto') : option === 'zh' ? t('settings.langZh') : t('settings.langEn'))), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.shareTitle') }), text(form.idleHours, 'idleHours', t('settings.fieldIdleHours'), t('settings.fieldIdleHoursHint'), t('settings.invalidHours'), true), bool(form.autoShareNewSessions, 'autoShareNewSessions', t('settings.fieldAutoShare'), t('settings.fieldAutoShareHint'))] }))] }) }), !clientRole && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-card", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", "data-down": data !== undefined && !data.gatewayReachable && !data.gatewayAbnormal ? 'true' : undefined, children: data === undefined
15913
+ }, onDiscard: () => { config.discard(); }, children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.roleTitle') }), select(form.role, 'role', t('settings.role'), t('settings.roleHint'), ['host', 'client'], (option) => (option === 'host' ? t('settings.roleHost') : t('settings.roleClient'))), text(form.serverName, 'serverName', t('settings.fieldServerName'), t(clientRole ? 'settings.fieldServerNameHintClient' : 'settings.fieldServerNameHint'), t('settings.invalidName')), reloadPending && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", children: t('settings.reloadNote') }), !clientRole && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.gatewayTitle') }), text(form.host, 'host', t('settings.fieldHost'), t('settings.fieldHostHint'), '', false, t('settings.fieldHostNote')), text(form.port, 'port', t('settings.fieldPort'), t('settings.fieldPortHint'), t('settings.invalidPort'), true), text(form.trustedProxies, 'trustedProxies', t('settings.fieldTrustedProxies'), t('settings.fieldTrustedProxiesHint'), ''), text(form.rateLimit, 'rateLimit', t('settings.fieldRateLimit'), t('settings.fieldRateLimitHint'), t('settings.invalidPositiveInt'), true), text(form.vapidSubject, 'vapidSubject', t('settings.fieldVapidSubject'), t('settings.fieldVapidSubjectHint'), ''), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.pushTitle') }), bool(form.pushSummary, 'pushSummary', t('settings.fieldPushSummary'), t('settings.fieldPushSummaryHint')), bool(form.pushTurnEnd, 'pushTurnEnd', t('settings.fieldPushTurnEnd'), t('settings.fieldPushTurnEndHint')), bool(form.pushTool, 'pushTool', t('settings.fieldPushTool'), t('settings.fieldPushToolHint')), text(form.pushDebounceMs, 'pushDebounceMs', t('settings.fieldPushDebounceMs'), t('settings.fieldPushDebounceMsHint'), t('settings.invalidNonNegativeInt'), true), select(form.lang, 'lang', t('settings.fieldLang'), t('settings.fieldLangHint'), ['auto', 'zh', 'en'], (option) => (option === 'auto' ? t('settings.langAuto') : option === 'zh' ? t('settings.langZh') : t('settings.langEn'))), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.shareTitle') }), text(form.idleHours, 'idleHours', t('settings.fieldIdleHours'), t('settings.fieldIdleHoursHint'), t('settings.invalidHours'), true), bool(form.autoShareNewSessions, 'autoShareNewSessions', t('settings.fieldAutoShare'), t('settings.fieldAutoShareHint')), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.bridge.title') }), bridgeState === 'loading' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.loading') }), bridgeState === 'error' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.loadFailed') }), bridgeState === 'empty' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.empty') }), bridgeLoad.rows.map((row) => {
15914
+ let current = {};
15915
+ try {
15916
+ const parsed = JSON.parse(form.pluginBridgeConsent.text);
15917
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
15918
+ current = parsed;
15919
+ }
15920
+ }
15921
+ catch { /* a malformed draft reads as nothing consented */ }
15922
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: row.plugin }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: row.version })] }), row.hasServerScope ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("label", { children: t('settings.bridge.consentToggle') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Switch, { checked: current[row.plugin] === true, onChange: (checked) => {
15923
+ config.stage('pluginBridgeConsent', JSON.stringify((0, plugin_bridge_ts_1.togglePluginConsent)(current, row.plugin, checked)));
15924
+ }, label: t('settings.bridge.consentToggle') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.consentHint') })] })) : ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.sessionOnly') }))] }, row.plugin));
15925
+ })] }))] }) }), !clientRole && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-card", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", "data-down": data !== undefined && !data.gatewayReachable && !data.gatewayAbnormal ? 'true' : undefined, children: data === undefined
15657
15926
  ? ''
15658
15927
  : data.gatewayAbnormal
15659
15928
  ? t('settings.gatewayAbnormal', { code: data.gatewayStatus })
@@ -15667,9 +15936,11 @@ function SettingsSectionPage({ config, shares, t }) {
15667
15936
  // hyphens stripped) on every keystroke, and only the
15668
15937
  // NORMALIZED length gates the pair button — a code pasted with
15669
15938
  // separators must survive pasting intact.
15670
- value: pairCode, onChange: (e) => { setPairCode((0, settings_form_ts_1.normalizePairingCode)(e.currentTarget.value)); } }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.client.pairingCodeHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: !claimAllowed, onClick: () => { void runClaim(); }, children: claimBusy ? t('settings.client.pairing') : t('settings.client.pair') }), form.serverName.invalid && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.fixDeviceName') }), claimFail !== null && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: pairFailText(claimFail, t) }), claimWriteFailed && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.failWrite') })] }), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.statusTitle') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 4 }, children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", style: { flex: 1 }, "data-down": clientView !== undefined && (clientView.state === 'unreachable' || clientView.state === 'unexpected' || clientView.state === 'revoked' || clientView.state === 'invalid-url' || clientView.state === 'incompatible') ? 'true' : undefined, children: clientView === undefined ? '' : clientStatusText(clientView, now, t) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientView === undefined || clientView.nextRetryAt === undefined || reconnectBusy, onClick: () => { void runReconnect(); }, children: reconnectBusy ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientLoad.state === 'loading', onClick: loadClientStatus, children: clientLoad.state === 'loading' ? t('settings.client.statusRefreshing') : t('settings.client.statusRefresh') })] }), form.deviceToken.configured && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: t('settings.client.tokenSet') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !form.available || !form.writable || unpairBusy, onClick: () => { void runUnpair(); }, children: unpairBusy ? t('settings.client.unpairBusy') : t('settings.client.unpair') })] })), unpairDone && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", style: { paddingBottom: 12 }, children: t('settings.client.unpairDone') }), clientView !== undefined && (clientView.lastError !== '' || clientView.intercept !== undefined || clientView.compat !== undefined) && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.diagTitle') }), clientView.lastError !== '' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": clientView.state === 'unreachable' ? 'true' : undefined, children: t('settings.client.diagLastError', { code: clientView.lastError }) })), clientView.intercept !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: clientView.intercept.installed
15939
+ value: pairCode, onChange: (e) => { setPairCode((0, settings_form_ts_1.normalizePairingCode)(e.currentTarget.value)); } }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.client.pairingCodeHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: !claimAllowed, onClick: () => { void runClaim(); }, children: claimBusy ? t('settings.client.pairing') : t('settings.client.pair') }), form.serverName.invalid && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.fixDeviceName') }), claimFail !== null && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: pairFailText(claimFail, t) }), claimWriteFailed && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.failWrite') })] }), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.statusTitle') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 4 }, children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", style: { flex: 1 }, "data-down": clientView !== undefined && (clientView.state === 'unreachable' || clientView.state === 'unexpected' || clientView.state === 'revoked' || clientView.state === 'invalid-url' || clientView.state === 'incompatible') ? 'true' : undefined, children: clientView === undefined ? '' : clientStatusText(clientView, now, t) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientView === undefined || clientView.nextRetryAt === undefined || reconnectBusy, onClick: () => { void runReconnect(); }, children: reconnectBusy ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientLoad.state === 'loading', onClick: loadClientStatus, children: clientLoad.state === 'loading' ? t('settings.client.statusRefreshing') : t('settings.client.statusRefresh') })] }), form.deviceToken.configured && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: t('settings.client.tokenSet') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !form.available || !form.writable || unpairBusy, onClick: () => { void runUnpair(); }, children: unpairBusy ? t('settings.client.unpairBusy') : t('settings.client.unpair') })] })), unpairDone && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", style: { paddingBottom: 12 }, children: t('settings.client.unpairDone') }), clientView !== undefined && (clientView.lastError !== '' || clientView.intercept !== undefined || clientView.compat !== undefined || (clientView.pluginBridge?.length ?? 0) > 0 || clientView.pluginRouteIntercept !== undefined) && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.diagTitle') }), clientView.lastError !== '' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": clientView.state === 'unreachable' ? 'true' : undefined, children: t('settings.client.diagLastError', { code: clientView.lastError }) })), clientView.intercept !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: clientView.intercept.installed
15671
15940
  ? t('settings.client.diagInterceptInstalled')
15672
- : t('settings.client.diagInterceptMissing') }), clientView.intercept.reasons.length > 0 && ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", "data-invalid": "true", children: [t('settings.client.diagInterceptDisabled'), clientView.intercept.reasons.map((reason, index) => ((0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", children: reason }, index)))] })), clientView.intercept.recentFailures.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagRecentFailures') }) }), clientView.intercept.recentFailures.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] })), clientView.compat !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [clientView.compat.mismatchedGroups.length > 0 && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.diagCompatGroups', { groups: clientView.compat.mismatchedGroups.join(', ') }) })), clientView.compat.recentCalls.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagCompatCalls') }) }), clientView.compat.recentCalls.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] }))] }))] }))] }));
15941
+ : t('settings.client.diagInterceptMissing') }), clientView.intercept.reasons.length > 0 && ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", "data-invalid": "true", children: [t('settings.client.diagInterceptDisabled'), clientView.intercept.reasons.map((reason, index) => ((0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", children: reason }, index)))] })), clientView.intercept.recentFailures.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagRecentFailures') }) }), clientView.intercept.recentFailures.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] })), clientView.compat !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [clientView.compat.mismatchedGroups.length > 0 && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.diagCompatGroups', { groups: clientView.compat.mismatchedGroups.join(', ') }) })), clientView.compat.recentCalls.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagCompatCalls') }) }), clientView.compat.recentCalls.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] })), (clientView.pluginBridge?.length ?? 0) > 0 && clientView.pluginBridge !== undefined && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.bridge.title') }) }), clientView.pluginBridge.map((row) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [row.plugin, " \u00B7 ", row.localVersion !== '' ? `v${row.localVersion}` : '—', " \u00B7", ' ', row.stateKey === 'settings.bridge.stateVersionMismatch'
15942
+ ? t(row.stateKey, { version: row.serverVersion ?? '?' })
15943
+ : t(row.stateKey)] }, row.plugin)))] })), clientView.pluginRouteIntercept !== undefined && !clientView.pluginRouteIntercept.installed && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.interceptOff', { reason: clientView.pluginRouteIntercept.reasons.join('; ') || '—' }) }))] }))] }))] }));
15673
15944
  }
15674
15945
  /** Display name of a value's source layer for the invalid-saved-value note. */
15675
15946
  function sourceName(source, t) {
@@ -16486,8 +16757,9 @@ __modules["client-data/remote-status.js"] = function (require, module, exports)
16486
16757
  * - `getRemoteStatusStore()` is the page-wide singleton.
16487
16758
  */
16488
16759
  Object.defineProperty(exports, "__esModule", { value: true });
16489
- exports.REMOTE_STATUS_POLL_MS = exports.documentVisibility = exports.CLIENT_UNSHARE_ROUTE = exports.REMOTE_STATUS_ROUTE = void 0;
16760
+ exports.REMOTE_STATUS_POLL_MS = exports.documentVisibility = exports.BRIDGE_STAMP_ATTRIBUTE = exports.BRIDGE_STAMP_PLUGIN = exports.CLIENT_UNSHARE_ROUTE = exports.REMOTE_STATUS_ROUTE = void 0;
16490
16761
  exports.parseRemoteStatusBody = parseRemoteStatusBody;
16762
+ exports.shouldStampBridgeAttribute = shouldStampBridgeAttribute;
16491
16763
  exports.createZhRemoteStatusFormatter = createZhRemoteStatusFormatter;
16492
16764
  exports.describeRemoteStatus = describeRemoteStatus;
16493
16765
  exports.bannerText = bannerText;
@@ -16521,9 +16793,30 @@ function parseRemoteStatusBody(body, now) {
16521
16793
  versionMismatch: record.versionMismatch === true,
16522
16794
  serverName: typeof record.serverName === 'string' ? record.serverName : '',
16523
16795
  closed,
16796
+ bridgedPlugins: Array.isArray(record.bridgedPlugins)
16797
+ ? record.bridgedPlugins.filter((name) => typeof name === 'string' && name !== '')
16798
+ : [],
16524
16799
  asOf: now,
16525
16800
  };
16526
16801
  }
16802
+ // --- the bridge stamp (PB-14) -----------------------------------------------
16803
+ /**
16804
+ * Whether ONE plugin's bridge verdict is bridged RIGHT NOW, judged off the
16805
+ * `bridgedPlugins` list the remote-status body carries. Tolerant by the
16806
+ * same discipline as the parse: a garbage list (an older server that never
16807
+ * learned the field, a broken body) reads as "not bridged" — the hiding
16808
+ * rules stay in force unless the server positively says otherwise, never
16809
+ * the reverse. This is the pure core behind the
16810
+ * `html[data-zr-bridge-<plugin>]` stamp the remote-session stylesheet
16811
+ * spends.
16812
+ */
16813
+ function shouldStampBridgeAttribute(bridgedPlugins, plugin) {
16814
+ return Array.isArray(bridgedPlugins) && bridgedPlugins.includes(plugin);
16815
+ }
16816
+ /** The plugin the bridge stamp names. */
16817
+ exports.BRIDGE_STAMP_PLUGIN = 'dsh-better-sidebar';
16818
+ /** The html attribute the stamp writes (`data-zr-bridge-dsh-better-sidebar`). */
16819
+ exports.BRIDGE_STAMP_ATTRIBUTE = `data-zr-bridge-${exports.BRIDGE_STAMP_PLUGIN}`;
16527
16820
  /** Default formatter: the plugin's Chinese dictionary. Components pass their
16528
16821
  * real framework `t`; this serves the pure-function callers (tests, check
16529
16822
  * scripts). */
@@ -17654,17 +17947,23 @@ html[data-zr-remote-session="1"] [data-open-target] {
17654
17947
  display: none !important;
17655
17948
  }
17656
17949
 
17657
- /* ---------- remote session: hide dsh-better-sidebar's file surfaces (T53) ----------
17658
- dsh-better-sidebar 0.24.x is the second big "open the machine's files" UI.
17659
- Every tab it registers (files/editor, git changes, subagent/tasks, side
17660
- chat, the markdown/html/code file viewers) builds its requests from a
17661
- {sessionId, cwd} scope and POSTs them to the plugin's own /sidebar/api/*
17662
- routes on whichever machine answers — in a remote session that is the
17663
- SUB-CLIENT's machine (client.js call() ~3111, fetchUpload ~3162,
17664
- encodeHtmlUrl /sidebar/html/ ~3069-3414), so the tree would show same-named
17665
- LOCAL files as if they were the session's and an edit would WRITE locally.
17666
- Per the T53 decision the interfaces are hidden, not proxied: the relay
17667
- carries no /sidebar routes and none are added.
17950
+ /* ---------- remote session: hide dsh-better-sidebar's file surfaces (T53, two regimes since PB-14) ----------
17951
+ dsh-better-sidebar 0.25.0 is the second big "open the machine's files" UI
17952
+ AND, since PB-12/13, the first ADAPTER-BRIDGED one: at exactly 0.25.0 its
17953
+ /sidebar/api routes, its two WebSockets and its side chat cross the byte
17954
+ tunnel and read/write the SERVER's data, gated on the whole-machine-data
17955
+ consent. The hiding below therefore applies ONLY while the plugin is NOT
17956
+ bridged — the :not([data-zr-bridge-dsh-better-sidebar="1"]) stamp the
17957
+ remote-status poll writes; see the PB-14 note above): bridged, the panels
17958
+ are remote-aware and stay visible; unbridged (a different version, the
17959
+ server without the adapter's consent), every tab it registers (files/
17960
+ editor, git changes, subagent/tasks, side chat, the markdown/html/code
17961
+ file viewers) builds its requests from a {sessionId, cwd} scope and POSTs
17962
+ them to the plugin's own /sidebar/api/* routes on whichever machine
17963
+ answers — in a remote session that is the SUB-CLIENT's machine (client.js
17964
+ call() ~3111, fetchUpload ~3162, encodeHtmlUrl /sidebar/html/ ~3069-3414),
17965
+ so the tree would show same-named LOCAL files as if they were the
17966
+ session's and an edit would WRITE locally.
17668
17967
 
17669
17968
  Anchors, all the plugin's or the host's own rendered contracts (no hashed
17670
17969
  class names; the one suffix selector, _chipIcon, is unique among the
@@ -17673,18 +17972,21 @@ html[data-zr-remote-session="1"] [data-open-target] {
17673
17972
  @liustack/modsearch / @wxg-prc-cpg/browser-skill-dsh-plugin (modsearch /
17674
17973
  browser-skill) / dshmarket and every @deepseek-ai sidebar package for it):
17675
17974
 
17676
- HONEST LIMITS (T53-fix): everything here is display:none ONLY. The plugin's
17975
+ HONEST LIMITS (T53-fix, adjusted PB-14): unbridged, the plugin's
17677
17976
  components stay mounted and keep silently POSTing READ-ONLY /sidebar/api
17678
17977
  requests to the local machine (session.cwd, changes.ops, the tree reads a
17679
17978
  mounted tab refreshes with) — the data never leaves that machine, and no
17680
- write can happen because every write control is inside the hidden surfaces.
17681
- Known boundary: a dsh-resource://file/** address opened through
17682
- sidebarRight.openResource in a remote session (file links in chat,
17683
- deliverables, @-references, skills) is CLAIMED by the plugin's editor
17684
- registration (client.js ~21377: patterns ["dsh-resource://file/**"]) before
17685
- the host preview sees it — the right sidebar expands, the rules below hide
17686
- the tab's body and chip, and an EMPTY panel remains. Root fix needs
17687
- zen-remote's own remote file tree; separate discussion.
17979
+ write can happen because every write control is inside the hidden
17980
+ surfaces (the tab body hides its CHILDREN and spends the note copy as a
17981
+ ::before, so an already-open tab reads WHY it is empty instead of
17982
+ sitting blank). Known boundary: a dsh-resource://file/** address opened
17983
+ through sidebarRight.openResource in a remote session (file links in
17984
+ chat, deliverables, @-references, skills) is CLAIMED by the plugin's
17985
+ editor registration (client.js ~21377: patterns ["dsh-resource://file/**"])
17986
+ before the host preview sees it — the right sidebar expands, the rules
17987
+ below hide the tab's body content and chip, and the note fills the panel.
17988
+ Bridged (0.25.0 + consent), none of this applies: the tab reads the
17989
+ server's data through the adapter and stays visible whole.
17688
17990
 
17689
17991
  - [data-dsh-bottom-toggle] — the header-utilities button that expands the
17690
17992
  bottom workbench (client.js BottomDockToggle ~20863; slot registration
@@ -17757,31 +18059,61 @@ html[data-zr-remote-session="1"] [data-open-target] {
17757
18059
  attribute and this rule hides it too (cosmetic; the page stays reachable
17758
18060
  through its shortcut command path and the plugin's takeover is the
17759
18061
  "files" implementation on every deployment this plugin pairs with
17760
- better-sidebar anyway). */
17761
- html[data-zr-remote-session="1"] [data-dsh-bottom-toggle] {
18062
+ better-sidebar anyway).
18063
+
18064
+ /* PB-14: every T53 rule below carries
18065
+ :not([data-zr-bridge-dsh-better-sidebar="1"]) — when the adapter
18066
+ (PB-12) bridges the plugin, the panels read the SERVER's data through
18067
+ the byte tunnel and MUST stay visible; the hiding applies only to the
18068
+ unbridged combination, and the empty tab body then carries a note
18069
+ instead of blankness. The stamp is written by
18070
+ effects/remote-session.ts off the remote-status poll's bridgedPlugins
18071
+ list (client-data/remote-status.ts shouldStampBridgeAttribute). */
18072
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-bottom-toggle] {
17762
18073
  display: none !important;
17763
18074
  }
17764
- html[data-zr-remote-session="1"] [data-dsh-panel-host] {
18075
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-panel-host] {
17765
18076
  display: none !important;
17766
18077
  }
17767
- html[data-zr-remote-session="1"] {
18078
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) {
17768
18079
  --dsh-sidebar-height: 0px !important;
17769
18080
  }
17770
- html[data-zr-remote-session="1"] [data-dsh-native-tab-host] {
18081
+ /* The unbridged tab body: hide the plugin's own content and spend the
18082
+ note copy as a ::before — an already-open tab reads WHY it is empty
18083
+ (0.25.0 keeps tabs mounted, so a stale body would otherwise sit blank).
18084
+ --zr-bridge-note is set on <html> by the stamp effect, in the
18085
+ browser's language; unset (effect not running yet) the content falls
18086
+ through and nothing renders. */
18087
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-native-tab-host] > * {
18088
+ display: none !important;
18089
+ }
18090
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-native-tab-host]::before {
18091
+ content: var(--zr-bridge-note);
18092
+ display: block;
18093
+ padding: 24px 16px;
18094
+ font-size: 13px;
18095
+ line-height: 1.7;
18096
+ color: var(--dsh-fg-muted, #6b7280);
18097
+ word-break: break-word;
18098
+ }
18099
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dockkit-tab]:has([class$="_chipIcon"]) {
17771
18100
  display: none !important;
17772
18101
  }
17773
- html[data-zr-remote-session="1"] [data-dockkit-tab]:has([class$="_chipIcon"]) {
18102
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-floating-window]:has([data-window-body]) {
17774
18103
  display: none !important;
17775
18104
  }
17776
- html[data-zr-remote-session="1"] [data-floating-window]:has([data-window-body]) {
18105
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="git"],
18106
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="subagent"],
18107
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="sidechat"] {
17777
18108
  display: none !important;
17778
18109
  }
17779
- html[data-zr-remote-session="1"] [data-sidebar-right-guide-entry="git"],
17780
- html[data-zr-remote-session="1"] [data-sidebar-right-guide-entry="subagent"],
17781
- html[data-zr-remote-session="1"] [data-sidebar-right-guide-entry="sidechat"] {
18110
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-sidebar-right-guide-entry="files"]:not([aria-keyshortcuts]) {
17782
18111
  display: none !important;
17783
18112
  }
17784
- html[data-zr-remote-session="1"] [data-sidebar-right-guide-entry="files"]:not([aria-keyshortcuts]) {
18113
+ /* 0.25.0's dock fallback — the floating quick-open button the plugin pins
18114
+ for empty workspaces. Same treatment as every other open-the-machine's-
18115
+ files control: hidden while unbridged, untouched when bridged. */
18116
+ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [data-dsh-dock-fallback] {
17785
18117
  display: none !important;
17786
18118
  }
17787
18119
 
@@ -17902,6 +18234,8 @@ exports.installRemoteSessionGuard = installRemoteSessionGuard;
17902
18234
  const types_ts_1 = require("./client/compat/types.js");
17903
18235
  const virtual_id_ts_1 = require("./virtual-id.js");
17904
18236
  const remote_session_css_ts_1 = require("./client/styles/remote-session.css.js");
18237
+ const remote_status_ts_1 = require("./client-data/remote-status.js");
18238
+ const locales_ts_1 = require("./client/locales.js");
17905
18239
  /**
17906
18240
  * The remote-session marker (T41b): the CURRENT main-view session id, read
17907
18241
  * the way the official uiSession `publishMain` reads it (RT
@@ -17924,6 +18258,26 @@ const remote_session_css_ts_1 = require("./client/styles/remote-session.css.js")
17924
18258
  * attribute must track the current session regardless of which panel is
17925
18259
  * mounted, and an attribute + stylesheet survives every re-render untouched.
17926
18260
  */
18261
+ /** The CURRENT main-view session id, the publishMain way (see the module
18262
+ * comment). Shared by the session marker and the bridge stamp — both key on
18263
+ * the same "which session is on screen" answer. */
18264
+ function currentMainSessionIdOf(ctx) {
18265
+ const { byId } = ctx.sessions.list.getSnapshot();
18266
+ const uiSession = ctx.get('uiSession');
18267
+ const current = uiSession?.current?.value?.key;
18268
+ const rows = byId;
18269
+ return typeof current === 'string' && (rows[current]?.retainedBy?.mainView ?? 0) > 0
18270
+ ? current
18271
+ : (0, types_ts_1.mainSessionIdOf)(byId);
18272
+ }
18273
+ /** The unbridged note copy in the BROWSER's language (the plugin's own
18274
+ * namespace carries both dictionaries; the framework `t` seat is not
18275
+ * reachable from a non-React effect, so the navigator's language picks —
18276
+ * `zh*` reads Chinese, everything else English). */
18277
+ function bridgeNoteOf(language) {
18278
+ const key = 'remoteBridgeUnbridgedNote';
18279
+ return typeof language === 'string' && language.startsWith('zh') ? locales_ts_1.zh[key] : locales_ts_1.en[key];
18280
+ }
17927
18281
  function installRemoteSessionGuard(ctx) {
17928
18282
  ctx.effect(() => {
17929
18283
  const tag = document.createElement('style');
@@ -17937,15 +18291,7 @@ function installRemoteSessionGuard(ctx) {
17937
18291
  }, 'dsh-zen-remote: remote-session styles');
17938
18292
  ctx.effect(() => {
17939
18293
  const apply = () => {
17940
- const { byId } = ctx.sessions.list.getSnapshot();
17941
- // publishMain's tie-break (see the module comment): prefer the still-
17942
- // retained current main binding, else the first mainView row.
17943
- const uiSession = ctx.get('uiSession');
17944
- const current = uiSession?.current?.value?.key;
17945
- const rows = byId;
17946
- const sessionId = typeof current === 'string' && (rows[current]?.retainedBy?.mainView ?? 0) > 0
17947
- ? current
17948
- : (0, types_ts_1.mainSessionIdOf)(byId);
18294
+ const sessionId = currentMainSessionIdOf(ctx);
17949
18295
  const root = document.documentElement;
17950
18296
  if (sessionId !== undefined && (0, virtual_id_ts_1.isVirtual)(sessionId))
17951
18297
  root.setAttribute('data-zr-remote-session', '1');
@@ -17966,6 +18312,64 @@ function installRemoteSessionGuard(ctx) {
17966
18312
  document.documentElement.removeAttribute('data-zr-remote-session');
17967
18313
  };
17968
18314
  }, 'dsh-zen-remote: remote-session attribute');
18315
+ // The bridge stamp (PB-14): while a REMOTE session is on screen, the
18316
+ // remote-status poll's `bridgedPlugins` decides whether better-sidebar is
18317
+ // bridged RIGHT NOW — bridged, the T53 hiding rules lift (the panels read
18318
+ // the server's data through the adapter) and the html carries
18319
+ // `data-zr-bridge-dsh-better-sidebar="1"`; not bridged, the rules stay
18320
+ // and the html carries the unbridged note copy as a CSS variable the
18321
+ // stylesheet spends on the empty tab body. The subscription rides
18322
+ // subscribeIfVirtual — a LOCAL session (or a host page) never polls, and
18323
+ // the attribute comes off; better-sidebar not installed or the server not
18324
+ // paired simply never appear in `bridgedPlugins`, so the stamp never
18325
+ // fires from this side alone.
18326
+ ctx.effect(() => {
18327
+ const root = document.documentElement;
18328
+ const store = (0, remote_status_ts_1.getRemoteStatusStore)();
18329
+ let offStore;
18330
+ const clear = () => {
18331
+ root.removeAttribute(remote_status_ts_1.BRIDGE_STAMP_ATTRIBUTE);
18332
+ root.style.removeProperty('--zr-bridge-note');
18333
+ };
18334
+ const applyBridge = () => {
18335
+ const view = store.getSnapshot().view;
18336
+ if ((0, remote_status_ts_1.shouldStampBridgeAttribute)(view?.bridgedPlugins, remote_status_ts_1.BRIDGE_STAMP_PLUGIN)) {
18337
+ root.setAttribute(remote_status_ts_1.BRIDGE_STAMP_ATTRIBUTE, '1');
18338
+ root.style.removeProperty('--zr-bridge-note');
18339
+ }
18340
+ else {
18341
+ root.removeAttribute(remote_status_ts_1.BRIDGE_STAMP_ATTRIBUTE);
18342
+ // JSON.stringify's quotes are a legal CSS string — the stylesheet's
18343
+ // ::before spends it as `content: var(--zr-bridge-note)`.
18344
+ root.style.setProperty('--zr-bridge-note', JSON.stringify(bridgeNoteOf(navigator.language)));
18345
+ }
18346
+ };
18347
+ const retune = () => {
18348
+ const sessionId = currentMainSessionIdOf(ctx);
18349
+ const virtualNow = sessionId !== undefined && (0, virtual_id_ts_1.isVirtual)(sessionId);
18350
+ if (virtualNow && offStore === undefined && typeof sessionId === 'string') {
18351
+ offStore = (0, remote_status_ts_1.subscribeIfVirtual)(store, sessionId, applyBridge);
18352
+ applyBridge();
18353
+ return;
18354
+ }
18355
+ if (!virtualNow && offStore !== undefined) {
18356
+ offStore();
18357
+ offStore = undefined;
18358
+ clear();
18359
+ }
18360
+ };
18361
+ retune();
18362
+ const offs = [ctx.sessions.list.subscribe(retune)];
18363
+ const current = ctx.get('uiSession')?.current;
18364
+ if (typeof current?.subscribe === 'function')
18365
+ offs.push(current.subscribe(retune));
18366
+ return () => {
18367
+ for (const off of offs)
18368
+ off();
18369
+ offStore?.();
18370
+ clear();
18371
+ };
18372
+ }, 'dsh-zen-remote: bridge stamp');
17969
18373
  }
17970
18374
  };
17971
18375
  __modules["client-data/model-group-side.js"] = function (require, module, exports) {