@linxin666/dsh-remote-web-ui 0.3.22 → 0.3.24

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 (43) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -0
  3. package/README.zh.md +4 -0
  4. package/lib/client.js +193 -20
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +181 -34
  7. package/lib/types/client/index.d.ts.map +1 -1
  8. package/lib/types/client/index.js +13 -18
  9. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  10. package/lib/types/client/mobile-adapt.js +40 -6
  11. package/lib/types/client/plugin-card-seat.d.ts +97 -0
  12. package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
  13. package/lib/types/client/plugin-card-seat.js +141 -0
  14. package/lib/types/client/remote-channel.d.ts +35 -0
  15. package/lib/types/client/remote-channel.d.ts.map +1 -1
  16. package/lib/types/client/remote-channel.js +55 -0
  17. package/lib/types/console-output.d.ts +41 -0
  18. package/lib/types/console-output.d.ts.map +1 -0
  19. package/lib/types/console-output.js +89 -0
  20. package/lib/types/index.d.ts.map +1 -1
  21. package/lib/types/index.js +26 -22
  22. package/lib/types/public-base.d.ts +67 -0
  23. package/lib/types/public-base.d.ts.map +1 -0
  24. package/lib/types/public-base.js +110 -0
  25. package/lib/types/remote-channel-boot.d.ts +5 -2
  26. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  27. package/lib/types/remote-channel-boot.js +28 -2
  28. package/lib/types/remote-channel-rules.d.ts +4 -0
  29. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  30. package/lib/types/remote-channel-rules.js +2 -0
  31. package/lib/types/update.d.ts.map +1 -1
  32. package/lib/types/update.js +5 -12
  33. package/package.json +1 -1
  34. package/src/client/index.ts +13 -17
  35. package/src/client/mobile-adapt.ts +39 -5
  36. package/src/client/plugin-card-seat.ts +185 -0
  37. package/src/client/remote-channel.ts +64 -0
  38. package/src/console-output.ts +97 -0
  39. package/src/index.ts +26 -22
  40. package/src/public-base.ts +122 -0
  41. package/src/remote-channel-boot.ts +28 -2
  42. package/src/remote-channel-rules.ts +6 -0
  43. package/src/update.ts +5 -10
package/lib/index.js CHANGED
@@ -3158,6 +3158,153 @@ var TunnelManager = class {
3158
3158
  } catch {}
3159
3159
  }
3160
3160
  };
3161
+ //#endregion
3162
+ //#region src/public-base.ts
3163
+ /**
3164
+ * The public base the pairing fence trusts while a tunnel is not running
3165
+ * (issue #1547).
3166
+ *
3167
+ * The fence reads exactly one public host: `service.publicBaseUrl`. Dropping
3168
+ * it the instant the tunnel leaves `running` makes every public request from
3169
+ * the address still printed in the QR code answer 403 — including
3170
+ * `/api/pair/status`, which exists to be callable before pairing. Two of the
3171
+ * three tunnel modes never lose their host: a named tunnel keeps its fixed
3172
+ * dashboard hostname, and a registered relay keeps its stable subdomain. A
3173
+ * quick tunnel does mint a new hostname on restart, so its previous host is
3174
+ * kept only for a bounded grace window: Cloudflare's edge can still deliver a
3175
+ * connection the phone already opened, and a mobile network reconnects
3176
+ * constantly, so an instant drop turns the phone's retry into a silent 403.
3177
+ * @module @linxin666/dsh-remote-web-ui/public-base
3178
+ */
3179
+ /** How long a quick-tunnel host stays trusted after the tunnel stops running. */
3180
+ const PUBLIC_BASE_GRACE_MS = 6e4;
3181
+ /**
3182
+ * Owns the trusted public base across tunnel and relay transitions.
3183
+ *
3184
+ * The keeper never decides on its own to widen trust: it only postpones
3185
+ * dropping an address the operator (named/relay) or the tunnel (quick) had
3186
+ * already published, and it forgets it once the tunnel comes back with the
3187
+ * address that actually serves the QR.
3188
+ */
3189
+ var PublicBaseKeeper = class {
3190
+ onChange;
3191
+ graceMs;
3192
+ mode = "off";
3193
+ tunnelUrl;
3194
+ relayUrl;
3195
+ graceTimer;
3196
+ /**
3197
+ * @param onChange - receives the base the fence should trust from now on.
3198
+ * @param graceMs - how long a quick-tunnel host outlives its tunnel.
3199
+ */
3200
+ constructor(onChange, graceMs = PUBLIC_BASE_GRACE_MS) {
3201
+ this.onChange = onChange;
3202
+ this.graceMs = graceMs;
3203
+ }
3204
+ /** The base a fresh fence read should trust: the stable relay origin wins. */
3205
+ current() {
3206
+ return this.relayUrl ?? this.tunnelUrl;
3207
+ }
3208
+ /** The mode that currently owns the public base. */
3209
+ setMode(mode) {
3210
+ this.mode = mode;
3211
+ }
3212
+ /** The last quick-tunnel URL, for callers that must announce it downstream. */
3213
+ quickUrl() {
3214
+ return this.tunnelUrl;
3215
+ }
3216
+ /** A relay registration result; undefined withdraws the stable origin. */
3217
+ setRelay(url) {
3218
+ this.relayUrl = url;
3219
+ this.emit();
3220
+ }
3221
+ /** The tunnel reported a live URL: it supersedes any pending grace. */
3222
+ markRunning(url) {
3223
+ this.cancelGrace();
3224
+ this.tunnelUrl = url;
3225
+ this.emit();
3226
+ }
3227
+ /**
3228
+ * The tunnel left `running` (starting again, or failed). A named tunnel's
3229
+ * fixed hostname stays trusted; a quick tunnel's previous host stays
3230
+ * trusted for the grace window, then is dropped if nothing replaced it.
3231
+ */
3232
+ markReconnecting() {
3233
+ if (this.mode === "named") return;
3234
+ this.startGrace();
3235
+ }
3236
+ /** Re-publish the current base after a branch change that did not move it. */
3237
+ refresh() {
3238
+ this.emit();
3239
+ }
3240
+ /** Forget everything (teardown, or a mode change that stops the tunnel). */
3241
+ reset() {
3242
+ this.cancelGrace();
3243
+ this.tunnelUrl = void 0;
3244
+ this.relayUrl = void 0;
3245
+ this.emit();
3246
+ }
3247
+ /** Cancel the pending grace timer; the keeper keeps its last base. */
3248
+ dispose() {
3249
+ this.cancelGrace();
3250
+ }
3251
+ startGrace() {
3252
+ if (this.tunnelUrl === void 0) return;
3253
+ if (this.graceTimer !== void 0) return;
3254
+ this.graceTimer = setTimeout(() => {
3255
+ this.graceTimer = void 0;
3256
+ this.tunnelUrl = void 0;
3257
+ this.emit();
3258
+ }, this.graceMs);
3259
+ }
3260
+ cancelGrace() {
3261
+ if (this.graceTimer === void 0) return;
3262
+ clearTimeout(this.graceTimer);
3263
+ this.graceTimer = void 0;
3264
+ }
3265
+ emit() {
3266
+ this.onChange(this.current());
3267
+ }
3268
+ };
3269
+ //#endregion
3270
+ //#region src/console-output.ts
3271
+ /** Count the U+FFFD replacement characters one candidate decode produced. */
3272
+ function replacementCount(text) {
3273
+ let count = 0;
3274
+ for (const character of text) if (character === "�") count += 1;
3275
+ return count;
3276
+ }
3277
+ /**
3278
+ * Decode one console byte buffer. Strict UTF-8 wins whenever it is valid;
3279
+ * otherwise the bytes are not UTF-8 at all and the console wrote its own code
3280
+ * page, so the candidate that loses the least text is taken.
3281
+ * @param bytes - the captured bytes.
3282
+ * @returns the decoded text.
3283
+ */
3284
+ function decodeConsoleBytes(bytes) {
3285
+ try {
3286
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3287
+ } catch {
3288
+ let best;
3289
+ let bestLoss = Number.POSITIVE_INFINITY;
3290
+ for (const label of ["gbk", "utf-8"]) {
3291
+ let text;
3292
+ try {
3293
+ text = new TextDecoder(label).decode(bytes);
3294
+ } catch {
3295
+ continue;
3296
+ }
3297
+ const loss = replacementCount(text);
3298
+ if (loss < bestLoss) {
3299
+ best = text;
3300
+ bestLoss = loss;
3301
+ }
3302
+ }
3303
+ /* v8 ignore next 3 -- reachable only without the legacy code page AND without the lenient pass */
3304
+ if (best === void 0) return Buffer.from(bytes).toString("utf8");
3305
+ return best;
3306
+ }
3307
+ }
3161
3308
  /** The aggregate package that is the canonical update entry point. */
3162
3309
  const AGGREGATE_PACKAGE = "@linxin666/dsh-web-all";
3163
3310
  /** Fallback anchor: this plugin's own package when the aggregate is absent. */
@@ -3512,18 +3659,12 @@ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|
3512
3659
  /**
3513
3660
  * Decode process output with tolerant decoding:
3514
3661
  * Defaults to UTF-8; if invalid byte sequences are encountered (common on
3515
- * Windows consoles using code page 936/GBK), falls back to GBK via TextDecoder.
3662
+ * Windows consoles using code page 936/GBK), falls back to the code page that
3663
+ * loses the least text. Bytes are decoded once per whole chunk by the caller's
3664
+ * accumulator — decoding per \`data\` event would split multi-byte characters.
3516
3665
  */
3517
3666
  function decodeProcessChunk(chunk) {
3518
- try {
3519
- return new TextDecoder("utf-8", { fatal: true }).decode(chunk);
3520
- } catch {
3521
- try {
3522
- return new TextDecoder("gbk").decode(chunk);
3523
- } catch {
3524
- return chunk.toString("utf8");
3525
- }
3526
- }
3667
+ return decodeConsoleBytes(chunk);
3527
3668
  }
3528
3669
  /**
3529
3670
  * Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
@@ -3859,7 +4000,9 @@ const REMOTE_CHANNEL_RULES = {
3859
4000
  ],
3860
4001
  deviceHeader: REMOTE_DEVICE_HEADER,
3861
4002
  deviceKey: "dsh-remote-device",
3862
- deviceQuery: REMOTE_DEVICE_QUERY
4003
+ deviceQuery: REMOTE_DEVICE_QUERY,
4004
+ uploadPath: "/api/session/uploadFileBinary",
4005
+ uploadHookGlobal: "__DSH_FILE_UPLOAD__"
3863
4006
  };
3864
4007
  /** The window global the boot patch publishes its seat under. */
3865
4008
  const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
@@ -3884,8 +4027,11 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
3884
4027
  * transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
3885
4028
  * connection plugin reads it: the paired remote desktop gets the full
3886
4029
  * settings/credentials/presets surface, and every call still rides the
3887
- * gated /remote channel. The script self-skips on loopback origins and
3888
- * never throws.
4030
+ * gated /remote channel. It finally publishes the official pre-Cordis
4031
+ * upload hook (`__DSH_FILE_UPLOAD__`), because the background upload
4032
+ * transport otherwise runs inside a Web Worker whose own globals the
4033
+ * main-thread rewrite cannot reach (issue #1580). The script self-skips on
4034
+ * loopback origins and never throws.
3889
4035
  * @module @linxin666/dsh-remote-web-ui/remote-channel-boot
3890
4036
  */
3891
4037
  /**
@@ -3896,7 +4042,7 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
3896
4042
  function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
3897
4043
  const json = JSON.stringify(rules);
3898
4044
  const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
3899
- return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
4045
+ return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);function uf(u,init){var raw=typeof u===\"string\"?u:u.href;if(typeof raw!==\"string\")return of.call(w,u,init||{});var q=new URL(raw,loc.href);if(so(q)&&q.pathname===R.uploadPath)return w.fetch(raw,init||{});return of.call(w,u,init||{})}try{if(w[R.uploadHookGlobal]===undefined)w[R.uploadHookGlobal]={fetch:uf}}catch(e){}seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
3900
4046
  }
3901
4047
  /** Boot-watchdog latch: one self-reload per session while the boot is broken. */
3902
4048
  const BOOT_WATCHDOG_KEY = "dsh-remote-boot-reload";
@@ -4079,17 +4225,20 @@ function applyImpl(ctx, config) {
4079
4225
  const service = new PairingService(pairingConfigOf(resolved));
4080
4226
  const tunnel = new TunnelManager();
4081
4227
  let relayRegistrar;
4082
- let relayUrl;
4083
- let rawTunnelUrl;
4228
+ /**
4229
+ * The public base the pairing fence trusts. A tunnel reconnect must not
4230
+ * strip the host the QR still shows: named and relay hosts never change,
4231
+ * and a quick tunnel's host survives a bounded grace window (issue #1547).
4232
+ */
4233
+ const publicBase = new PublicBaseKeeper((base) => {
4234
+ service.setPublicBaseUrl(base);
4235
+ });
4084
4236
  /** The tunnel target the registrar last announced (dedupes sync re-runs). */
4085
4237
  let relayAnnouncedFor;
4086
- const setPublicBase = () => {
4087
- service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
4088
- };
4089
4238
  const disposeRelayRegistrar = (unregister = false) => {
4090
4239
  const registrar = relayRegistrar;
4091
4240
  relayRegistrar = void 0;
4092
- relayUrl = void 0;
4241
+ publicBase.setRelay(void 0);
4093
4242
  relayAnnouncedFor = void 0;
4094
4243
  if (registrar === void 0) return;
4095
4244
  if (unregister) registrar.unregister().catch(() => void 0);
@@ -4100,10 +4249,9 @@ function applyImpl(ctx, config) {
4100
4249
  if (relayRegistrar === void 0) try {
4101
4250
  relayRegistrar = new RelayRegistrar(loadRelayIdentity(resolve().profile), (state) => {
4102
4251
  service.setRelayStatus(state.state === "off" ? void 0 : state);
4103
- if (state.state === "running") relayUrl = state.url;
4104
- else if (state.state === "off") relayUrl = void 0;
4252
+ if (state.state === "running") publicBase.setRelay(state.url);
4253
+ else if (state.state === "off") publicBase.setRelay(void 0);
4105
4254
  else if (state.state === "failed") console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
4106
- setPublicBase();
4107
4255
  });
4108
4256
  } catch (error) {
4109
4257
  console.warn(`remote-web-ui: relay registry unavailable (${error instanceof Error ? error.message : String(error)}) — the quick URL is the QR base`);
@@ -4119,8 +4267,7 @@ function applyImpl(ctx, config) {
4119
4267
  tunnel.onPhase((info) => {
4120
4268
  if (tunnelMode === "off") return;
4121
4269
  if (info.phase === "running" && info.url !== void 0) {
4122
- rawTunnelUrl = info.url;
4123
- setPublicBase();
4270
+ publicBase.markRunning(info.url);
4124
4271
  service.setTunnelStatus({
4125
4272
  state: "running",
4126
4273
  url: info.url
@@ -4129,13 +4276,10 @@ function applyImpl(ctx, config) {
4129
4276
  if (registrar !== void 0) announceRelay(registrar, info.url);
4130
4277
  runPostureProbe();
4131
4278
  } else if (info.phase === "starting") {
4132
- rawTunnelUrl = void 0;
4133
- relayUrl = void 0;
4134
- setPublicBase();
4279
+ publicBase.markReconnecting();
4135
4280
  service.setTunnelStatus({ state: "starting" });
4136
4281
  } else if (info.phase === "failed") {
4137
- rawTunnelUrl = void 0;
4138
- setPublicBase();
4282
+ publicBase.markReconnecting();
4139
4283
  service.setTunnelStatus(info.error === void 0 ? { state: "failed" } : {
4140
4284
  state: "failed",
4141
4285
  error: info.error
@@ -4144,6 +4288,7 @@ function applyImpl(ctx, config) {
4144
4288
  });
4145
4289
  ctx.effect(() => () => {
4146
4290
  disposeRelayRegistrar();
4291
+ publicBase.dispose();
4147
4292
  tunnel.dispose();
4148
4293
  }, "remote-web-ui: auto tunnel");
4149
4294
  if ((ctx.webServer.host === "0.0.0.0" || ctx.webServer.host === "127.0.0.1") && Number.isFinite(ctx.webServer.port)) {
@@ -4391,15 +4536,17 @@ function applyImpl(ctx, config) {
4391
4536
  }
4392
4537
  const plan = tunnelPlanOf(value, ctx.webServer.port);
4393
4538
  tunnelMode = plan.mode;
4539
+ publicBase.setMode(plan.mode);
4540
+ const liveTunnelUrl = publicBase.quickUrl();
4394
4541
  if (plan.mode !== "quick") {
4395
4542
  disposeRelayRegistrar();
4396
- if (plan.mode !== "named") setPublicBase();
4543
+ if (plan.mode !== "named") publicBase.refresh();
4397
4544
  } else if (value.relay === false) {
4398
4545
  disposeRelayRegistrar(true);
4399
- setPublicBase();
4400
- } else if (rawTunnelUrl !== void 0) {
4546
+ publicBase.refresh();
4547
+ } else if (liveTunnelUrl !== void 0) {
4401
4548
  const registrar = ensureRelayRegistrar();
4402
- if (registrar !== void 0) announceRelay(registrar, rawTunnelUrl);
4549
+ if (registrar !== void 0) announceRelay(registrar, liveTunnelUrl);
4403
4550
  }
4404
4551
  if (plan.mode === "quick") {
4405
4552
  for (const ignored of plan.ignored) console.warn(`remote-web-ui: autoTunnel is on — ignoring the configured ${ignored}`);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAelG,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAmBrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAiR9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAelG,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAoBrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA4Q9C"}
@@ -19,6 +19,7 @@ import { channelTransition, installRemoteChannel, isLoopbackHostname, remoteChan
19
19
  import { FenceNotice } from "./FenceNotice.js";
20
20
  import { reportDailyHeartbeat } from "./telemetry.js";
21
21
  import { startMobileAdapt } from "./mobile-adapt.js";
22
+ import { installPluginCard } from "./plugin-card-seat.js";
22
23
  /** Dictionary namespace owned by this plugin. */
23
24
  const NS = 'remote';
24
25
  /** Settings namespace the remote-control card edits (the Host plugin registers it). */
@@ -150,26 +151,20 @@ export function apply(ctx) {
150
151
  };
151
152
  });
152
153
  // Plugin configuration card: one staged form over the `remote-web-ui`
153
- // settings namespace, contributed to the Web UI plugin group.
154
+ // settings namespace, contributed to whichever plugin-card seat the running
155
+ // host declares (the family group's list seat, or the official keyed seat of
156
+ // the plugin-configuration tab when dsh-web-settings is not installed —
157
+ // issue #1589).
154
158
  const remoteSettings = new RemoteSettingsCardController(settingsScope);
155
- ctx.slots.inject('web-ui.plugin.item', () => {
156
- try {
157
- const unregister = ctx.slots.register({
158
- name: 'web-ui.plugin.item',
159
- id: 'remote-web-ui',
160
- order: 90,
161
- locale: NS,
162
- inject: () => remoteSettings.inject(),
163
- }, RemoteSettingsCard);
164
- return () => {
165
- remoteSettings.dispose();
166
- unregister();
167
- };
168
- }
169
- catch {
170
- return () => { };
171
- }
159
+ installPluginCard(ctx, {
160
+ namespace: REMOTE_WEB_UI_NS,
161
+ id: 'remote-web-ui',
162
+ order: 90,
163
+ locale: NS,
164
+ inject: () => remoteSettings.inject(),
165
+ component: RemoteSettingsCard,
172
166
  });
167
+ ctx.effect(() => () => { remoteSettings.dispose(); }, 'remote-web-ui: settings card');
173
168
  // Phone-side boot flow + heartbeats. Loopback pages (the desktop) never
174
169
  // heartbeat; the server ignores unpaired heartbeats anyway. Both run only
175
170
  // while the plugin is enabled.
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AA+PD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CA0zBvC"}
1
+ {"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AAmRD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAw0BvC"}
@@ -40,6 +40,12 @@ const EFFORT_BTN_ID = 'dshRemoteEffortPick';
40
40
  const COMPACT_CLASS = 'dsh-remote-compact-picker';
41
41
  /** Body class while the header actions are seated in the tabs row. */
42
42
  const HEADER_SEATED_CLASS = 'dsh-remote-header-seated';
43
+ /**
44
+ * Width the seated header actions paint over the tabs row, as a CSS variable
45
+ * on <html>. The row caps its own width with it (see the v80 rules) instead of
46
+ * reserving the space with padding.
47
+ */
48
+ const HEADER_RESERVE_VAR = '--dsh-remote-header-actions-reserve';
43
49
  /** Locale-dependent fast path for the official picker cells (zh/en). */
44
50
  const PICKER_CELL_PATTERN = {
45
51
  model: /模型|Model/, // i18n-allow: matches official picker cell text, not plugin copy
@@ -228,6 +234,20 @@ const ADAPT_CSS = [
228
234
  // [class$="_tab"] misses the active tab (its class ends in "_tabActive")
229
235
  // — containment so BOTH tabs match.
230
236
  '[class$="_header"] [class$="_tabs"] [class*="_tab"]{font-size:12px;white-space:nowrap}',
237
+ // v80: the tabs row must give the painted actions their width up on its own.
238
+ // The v67 padding reservation could not do it: the row is a nowrap flex
239
+ // container, and once the tab labels plus their gaps overflow the reduced
240
+ // content box the flex items spill over the padding box, so the trailing
241
+ // tabs render underneath the actions (reported on a phone: a long agent
242
+ // preset label covered the last tabs). Cap the row against the reserve and
243
+ // let the labels scroll instead. The value rides a CSS variable on <html>
244
+ // because the update lands before React re-creates the row node, and an
245
+ // inline reservation on that node is dropped until the next sync tick.
246
+ // The 2px bottom padding keeps the active-tab underline inside the clipping
247
+ // box; the matching negative margin keeps the header height official.
248
+ `body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]{max-width:max(0px,calc(100% - var(${HEADER_RESERVE_VAR},0px)));overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;padding-bottom:2px;margin-bottom:-2px}`,
249
+ `body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]::-webkit-scrollbar{display:none}`,
250
+ `body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"] > [class*="_tab"]{flex:0 0 auto}`,
231
251
  // Mobile scope: hide the plugin surfaces that do not fit a phone — the
232
252
  // right-hand details column and every desktop-oriented tool surface. The
233
253
  // list keys on the L2 semantic roots (data-dsh-plugin, ownership stays
@@ -775,9 +795,14 @@ export function startMobileAdapt() {
775
795
  let dx = translate !== null ? parseFloat(translate[1] ?? '0') : 0;
776
796
  let dy = translate !== null ? parseFloat(translate[2] ?? '0') : 0;
777
797
  const actionsRect = actions.getBoundingClientRect();
778
- const tabsRect = tabs.getBoundingClientRect();
779
- // Horizontal: the actions' right edge lands on the tabs row's right edge.
780
- const rightDiff = tabsRect.right - actionsRect.right;
798
+ // Horizontal: the actions' right edge lands on the header's content edge,
799
+ // NOT on the tabs row's right edge. The row caps its own width against the
800
+ // reserve below (v80), so anchoring to the row would let the pair walk
801
+ // leftwards on every measurement.
802
+ const headerRect = header.getBoundingClientRect();
803
+ const headerStyle = getComputedStyle(header);
804
+ const contentRight = headerRect.right - (parseFloat(headerStyle.paddingRight) || 0) - (parseFloat(headerStyle.borderRightWidth) || 0);
805
+ const rightDiff = contentRight - actionsRect.right;
781
806
  if (Math.abs(rightDiff) >= 0.5)
782
807
  dx = Math.round((dx + rightDiff) * 10) / 10;
783
808
  // Vertical: the actions' text bottom lands on the tab text bottom.
@@ -798,18 +823,27 @@ export function startMobileAdapt() {
798
823
  const next = `translate(${dx}px, ${dy}px)`;
799
824
  if (actions.style.transform !== next)
800
825
  actions.style.transform = next;
801
- // Reserve the painted width so the tab labels never slide under it.
826
+ // Reserve the painted width on the row itself, so the tab labels never
827
+ // slide under it. The value rides a CSS variable on <html>: React
828
+ // recreates the row node on re-render, which would drop an inline
829
+ // reservation until the next sync tick, and until then the labels render
830
+ // underneath the painted actions.
802
831
  const reserve = `${Math.ceil(actionsRect.width) + 8}px`;
803
- if (tabs.style.paddingRight !== reserve)
804
- tabs.style.paddingRight = reserve;
832
+ const rootStyle = document.documentElement.style;
833
+ if (rootStyle.getPropertyValue(HEADER_RESERVE_VAR) !== reserve)
834
+ rootStyle.setProperty(HEADER_RESERVE_VAR, reserve);
805
835
  }
806
836
  function unseatHeaderActions() {
807
837
  document.body.classList.remove(HEADER_SEATED_CLASS);
838
+ document.documentElement.style.removeProperty(HEADER_RESERVE_VAR);
808
839
  const header = document.querySelector('[class$="_header"]');
809
840
  const tabs = header !== null ? header.querySelector('[class$="_tabs"]') : null;
810
841
  const actions = header !== null ? header.querySelector('[class$="_titleCluster"] [class$="_headerActions"]') : null;
811
842
  if (actions instanceof HTMLElement)
812
843
  actions.style.transform = '';
844
+ // The v67 padding reservation is gone with the v80 cap; clearing it keeps a
845
+ // profile that hot-updated from an older build from keeping a stale inline
846
+ // width on the row.
813
847
  if (tabs instanceof HTMLElement)
814
848
  tabs.style.paddingRight = '';
815
849
  if (headerObserver !== null) {
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Family plugin-card seat.
3
+ *
4
+ * A family plugin contributes its settings card to whichever plugin-card seat
5
+ * the running host actually renders:
6
+ *
7
+ * - `web-ui.plugin.item` — the list seat declared by the dsh-web-settings
8
+ * group section (this family's own first-level "Web UI plugins" section);
9
+ * - `settings.plugin.item` — the official keyed seat of the harness's
10
+ * `ui-settings-plugins` tab, keyed by the settings namespace the card edits.
11
+ *
12
+ * SEAT SELECTION IS NOT A DECLARATION PROBE. The official `ui-settings-plugins`
13
+ * row belongs to the harness bundle and its `configurable` tab always declares
14
+ * `settings.plugin.item` before any external plugin's `apply()` runs, so
15
+ * "is the official seat declared?" answers yes even in the deployment whose
16
+ * whole point is the family group. Choosing on that probe sends every family
17
+ * card to the official Plugins tab and leaves the group's own section
18
+ * permanently empty — the family of reports where the section renders its
19
+ * heading and zero cards.
20
+ *
21
+ * The signal that actually distinguishes the two deployments is whether
22
+ * dsh-web-settings is loaded: it is the package that owns the group section and
23
+ * it publishes the `webUiSettings` service during `apply()`, which every
24
+ * family plugin already reads for its settings scope. Group loaded -> the family
25
+ * seat; group absent -> the official seat.
26
+ *
27
+ * The decision is re-evaluated on every `slots/changed` because the group may
28
+ * apply after this plugin (the family aggregate orders it first, a profile that
29
+ * installs the group separately need not): the initial contribution goes to the
30
+ * official seat, then moves to the family seat the moment the group's section
31
+ * registers. The entry is disposed before the replacement is registered, so a
32
+ * card is never in two seats at once.
33
+ *
34
+ * The shared tree has no client-SDK dependency, so this module reads its
35
+ * context through the structural shape below; callers pass the plugin's own
36
+ * `ctx`.
37
+ */
38
+ /** The family list seat key. */
39
+ export declare const FAMILY_PLUGIN_CARD_SEAT = "web-ui.plugin.item";
40
+ /** The official keyed plugin-card seat key. */
41
+ export declare const OFFICIAL_PLUGIN_CARD_SEAT = "settings.plugin.item";
42
+ /** The service dsh-web-settings publishes while it is loaded. */
43
+ export declare const FAMILY_GROUP_SERVICE = "webUiSettings";
44
+ /** The slot-registry member this helper uses (structurally satisfied by ctx.slots). */
45
+ export interface PluginCardSlots {
46
+ /** Contribution of one card entry. */
47
+ register(options: never, component: never): unknown;
48
+ }
49
+ /** The slice of the client context a card contribution needs. */
50
+ export interface PluginCardContext {
51
+ slots: PluginCardSlots;
52
+ /** Service lookup; absent on a context double that only models the slots. */
53
+ get?(name: string): unknown;
54
+ /** Event subscription seat; absent on an event-less test double. */
55
+ on?(event: string, listener: (...args: never[]) => void): unknown;
56
+ }
57
+ /** Owner share of a plugin card (both seats supply nothing). */
58
+ export interface SettingsPluginItemOwnerProps {
59
+ /** Marker field: card owner props are intentionally empty. */
60
+ children?: never;
61
+ }
62
+ /** One family plugin's card contribution. */
63
+ export interface PluginCardSeat {
64
+ /** Settings namespace the card edits (the official seat's dispatch key). */
65
+ namespace: string;
66
+ /** Family list-seat entry id. */
67
+ id: string;
68
+ /** Family list-seat sort order. */
69
+ order?: number;
70
+ /** Family list-seat display label; the official keyed seat carries none. */
71
+ label?: () => string;
72
+ /** Locale namespace the card renders with. */
73
+ locale: string;
74
+ /** Business-face factory of the registration. */
75
+ inject?: () => object;
76
+ /**
77
+ * The card component. Its props type is the seat's composed shape, which is
78
+ * chosen at runtime, so the helper takes it erased; the registrant keeps its
79
+ * own precise typing at the call site.
80
+ */
81
+ component: unknown;
82
+ }
83
+ /**
84
+ * Whether the family group (dsh-web-settings) is loaded in this page. The
85
+ * service is the group package's own contract, so the probe cannot be fooled
86
+ * by a harness release that starts declaring the official seat differently.
87
+ */
88
+ export declare function familyGroupLoaded(ctx: PluginCardContext): boolean;
89
+ /**
90
+ * Contribute one family plugin card to the seat this host renders, following
91
+ * the group if it loads later. The entry is disposed and re-registered on a
92
+ * seat change, never duplicated.
93
+ * @param ctx - client context (its slot registry decides the seat).
94
+ * @param seat - the card contribution.
95
+ */
96
+ export declare function installPluginCard(ctx: PluginCardContext, seat: PluginCardSeat): void;
97
+ //# sourceMappingURL=plugin-card-seat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-card-seat.d.ts","sourceRoot":"","sources":["../../../src/client/plugin-card-seat.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,gCAAgC;AAChC,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAE3D,+CAA+C;AAC/C,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAE/D,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,kBAAkB,CAAA;AAEnD,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,GAAG,OAAO,CAAA;CACpD;AAED,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAA;IACtB,6EAA6E;IAC7E,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,oEAAoE;IACpE,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;CAClE;AAED,gEAAgE;AAChE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;IACpB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;IACrB;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAUjE;AAWD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CA4DpF"}