@linxin666/dsh-remote-web-ui 0.4.3 → 0.4.5

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 (47) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +23 -8
  3. package/README.zh.md +22 -7
  4. package/lib/client.js +194 -140
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +231 -23
  7. package/lib/types/client/PluginSettingsCard.d.ts +7 -0
  8. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.js +2 -2
  10. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/RemoteSettingsCard.js +2 -1
  12. package/lib/types/client/index.js +3 -3
  13. package/lib/types/client/locales.d.ts +0 -4
  14. package/lib/types/client/locales.d.ts.map +1 -1
  15. package/lib/types/client/locales.js +5 -9
  16. package/lib/types/client/mobile-adapt.d.ts +2 -2
  17. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  18. package/lib/types/client/mobile-adapt.js +163 -136
  19. package/lib/types/client/settings-form.d.ts +16 -0
  20. package/lib/types/client/settings-form.d.ts.map +1 -1
  21. package/lib/types/client/settings-form.js +27 -1
  22. package/lib/types/detached-work.d.ts +45 -0
  23. package/lib/types/detached-work.d.ts.map +1 -0
  24. package/lib/types/detached-work.js +54 -0
  25. package/lib/types/index.d.ts.map +1 -1
  26. package/lib/types/index.js +67 -12
  27. package/lib/types/lan-bind.d.ts +21 -4
  28. package/lib/types/lan-bind.d.ts.map +1 -1
  29. package/lib/types/lan-bind.js +103 -17
  30. package/lib/types/pairing.d.ts.map +1 -1
  31. package/lib/types/pairing.js +43 -0
  32. package/lib/types/remote-api.d.ts +13 -1
  33. package/lib/types/remote-api.d.ts.map +1 -1
  34. package/lib/types/remote-api.js +14 -1
  35. package/package.json +19 -19
  36. package/src/client/PluginSettingsCard.tsx +9 -0
  37. package/src/client/RemoteSettingsCard.tsx +2 -0
  38. package/src/client/index.ts +3 -3
  39. package/src/client/locales.ts +5 -9
  40. package/src/client/mobile-adapt.ts +167 -138
  41. package/src/client/settings-card.module.css +31 -5
  42. package/src/client/settings-form.ts +27 -1
  43. package/src/detached-work.ts +57 -0
  44. package/src/index.ts +67 -12
  45. package/src/lan-bind.ts +111 -17
  46. package/src/pairing.ts +44 -1
  47. package/src/remote-api.ts +14 -1
package/lib/client.js CHANGED
@@ -2114,7 +2114,7 @@ window.__ModuleLoader__.load({
2114
2114
  }
2115
2115
  //#endregion
2116
2116
  //#region \0dsh-css:packages/dsh-remote-web-ui/src/client/settings-card.module.css.mjs
2117
- const css = ".Kwoi6G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Kwoi6G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_header{appearance:none;box-sizing:border-box;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Kwoi6G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Kwoi6G_headerStatic{box-sizing:border-box;border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Kwoi6G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Kwoi6G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Kwoi6G_description{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5}.Kwoi6G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Kwoi6G_chevronOpen{transform:rotate(180deg)}.Kwoi6G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Kwoi6G_readOnly{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Kwoi6G_failed{min-width:0;color:var(--dsw-alias-state-error-primary,#b42318);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Kwoi6G_discard,.Kwoi6G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Kwoi6G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Kwoi6G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Kwoi6G_discard:disabled,.Kwoi6G_save:disabled{opacity:.4;cursor:default}.Kwoi6G_discard:focus-visible,.Kwoi6G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Kwoi6G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Kwoi6G_field+.Kwoi6G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Kwoi6G_head{align-items:center;gap:8px;display:flex}.Kwoi6G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Kwoi6G_badges{align-items:center;gap:8px;display:inline-flex}.Kwoi6G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Kwoi6G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Kwoi6G_reset:disabled{cursor:default}.Kwoi6G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Kwoi6G_input,.Kwoi6G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_input:focus-visible,.Kwoi6G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Kwoi6G_input:disabled,.Kwoi6G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Kwoi6G_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary,#b42318);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary,#b42318);outline-offset:1px;border-color:var(--dsw-alias-state-error-primary,#b42318)}.Kwoi6G_selectWrap{position:relative}.Kwoi6G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Kwoi6G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Kwoi6G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Kwoi6G_selectChevronOpen{transform:rotate(180deg)}.Kwoi6G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Kwoi6G_selectPopupOpen{opacity:1;transform:none}.Kwoi6G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Kwoi6G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Kwoi6G_selectOption:hover,.Kwoi6G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Kwoi6G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Kwoi6G_invalid{color:var(--dsw-alias-state-error-primary,#b42318);margin:0;font-size:12px;line-height:1.5}.Kwoi6G_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Kwoi6G_card,.Kwoi6G_header,.Kwoi6G_chevron,.Kwoi6G_chevronOpen,.Kwoi6G_discard,.Kwoi6G_save,.Kwoi6G_selectChevron,.Kwoi6G_selectChevronOpen,.Kwoi6G_selectPopup{transition:none}}";
2117
+ const css = ".Kwoi6G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Kwoi6G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_header{appearance:none;box-sizing:border-box;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:13px;padding:16px;display:flex}.Kwoi6G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Kwoi6G_headerStatic{box-sizing:border-box;border-radius:12px;align-items:center;gap:13px;width:100%;padding:16px;display:flex}.Kwoi6G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Kwoi6G_mark{background:var(--dsw-alias-bg-module-platform);width:34px;height:34px;color:var(--dsw-alias-label-secondary);border-radius:10px;flex:none;place-items:center;transition:color .16s;display:grid}.Kwoi6G_header:hover .Kwoi6G_mark,.Kwoi6G_cardOpen .Kwoi6G_mark{color:var(--dsw-alias-label-primary)}.Kwoi6G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Kwoi6G_description{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5}.Kwoi6G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;width:16px;height:16px;transition:transform .16s,color .16s}.Kwoi6G_header:hover .Kwoi6G_chevron{color:var(--dsw-alias-label-secondary)}.Kwoi6G_chevronOpen{transform:rotate(180deg)}.Kwoi6G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Kwoi6G_readOnly{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Kwoi6G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Kwoi6G_failed{min-width:0;color:var(--dsw-alias-state-error-primary,#b42318);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Kwoi6G_discard,.Kwoi6G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Kwoi6G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Kwoi6G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Kwoi6G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Kwoi6G_discard:disabled,.Kwoi6G_save:disabled{opacity:.4;cursor:default}.Kwoi6G_discard:focus-visible,.Kwoi6G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Kwoi6G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Kwoi6G_field+.Kwoi6G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Kwoi6G_head{align-items:center;gap:8px;display:flex}.Kwoi6G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Kwoi6G_badges{align-items:center;gap:8px;display:inline-flex}.Kwoi6G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Kwoi6G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Kwoi6G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Kwoi6G_reset:disabled{cursor:default}.Kwoi6G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Kwoi6G_input,.Kwoi6G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_input:focus-visible,.Kwoi6G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Kwoi6G_input:disabled,.Kwoi6G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Kwoi6G_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary,#b42318);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Kwoi6G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-state-error-primary,#b42318);outline-offset:1px;border-color:var(--dsw-alias-state-error-primary,#b42318)}.Kwoi6G_selectWrap{position:relative}.Kwoi6G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Kwoi6G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Kwoi6G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Kwoi6G_selectChevronOpen{transform:rotate(180deg)}.Kwoi6G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Kwoi6G_selectPopupOpen{opacity:1;transform:none}.Kwoi6G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Kwoi6G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Kwoi6G_selectOption:hover,.Kwoi6G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Kwoi6G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Kwoi6G_invalid{color:var(--dsw-alias-state-error-primary,#b42318);margin:0;font-size:12px;line-height:1.5}.Kwoi6G_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Kwoi6G_card,.Kwoi6G_header,.Kwoi6G_mark,.Kwoi6G_chevron,.Kwoi6G_chevronOpen,.Kwoi6G_discard,.Kwoi6G_save,.Kwoi6G_selectChevron,.Kwoi6G_selectChevronOpen,.Kwoi6G_selectPopup{transition:none}}";
2118
2118
  const tagId = "@linxin666/dsh-remote-web-ui/packages/dsh-remote-web-ui/src/client/settings-card.module.css";
2119
2119
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2120
2120
  const tag = document.createElement("style");
@@ -2145,6 +2145,7 @@ window.__ModuleLoader__.load({
2145
2145
  "inputInvalid": "Kwoi6G_inputInvalid",
2146
2146
  "invalid": "Kwoi6G_invalid",
2147
2147
  "label": "Kwoi6G_label",
2148
+ "mark": "Kwoi6G_mark",
2148
2149
  "name": "Kwoi6G_name",
2149
2150
  "notExposed": "Kwoi6G_notExposed",
2150
2151
  "pending": "Kwoi6G_pending",
@@ -2190,22 +2191,30 @@ window.__ModuleLoader__.load({
2190
2191
  const cardClass = expanded ? `${settings_card_module_css_default.cardOpen} ${settings_card_module_css_default.card}` : settings_card_module_css_default.card;
2191
2192
  const header = alwaysOpen === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2192
2193
  className: settings_card_module_css_default.headerStatic,
2193
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2194
- className: settings_card_module_css_default.headText,
2195
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2196
- className: settings_card_module_css_default.name,
2197
- title,
2198
- children: title
2199
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2200
- className: settings_card_module_css_default.description,
2201
- title: description,
2202
- children: props.descriptionNode ?? description
2203
- })]
2204
- }), state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2205
- className: settings_card_module_css_default.pending,
2206
- title: props.t("settings.unsaved"),
2207
- children: props.t("settings.unsaved")
2208
- }) : null]
2194
+ children: [
2195
+ props.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2196
+ className: settings_card_module_css_default.mark,
2197
+ "aria-hidden": "true",
2198
+ children: props.icon
2199
+ }) : null,
2200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2201
+ className: settings_card_module_css_default.headText,
2202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2203
+ className: settings_card_module_css_default.name,
2204
+ title,
2205
+ children: title
2206
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2207
+ className: settings_card_module_css_default.description,
2208
+ title: description,
2209
+ children: props.descriptionNode ?? description
2210
+ })]
2211
+ }),
2212
+ state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2213
+ className: settings_card_module_css_default.pending,
2214
+ title: props.t("settings.unsaved"),
2215
+ children: props.t("settings.unsaved")
2216
+ }) : null
2217
+ ]
2209
2218
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2210
2219
  type: "button",
2211
2220
  className: settings_card_module_css_default.header,
@@ -2215,6 +2224,11 @@ window.__ModuleLoader__.load({
2215
2224
  setOpen(!open);
2216
2225
  },
2217
2226
  children: [
2227
+ props.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2228
+ className: settings_card_module_css_default.mark,
2229
+ "aria-hidden": "true",
2230
+ children: props.icon
2231
+ }) : null,
2218
2232
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2219
2233
  className: settings_card_module_css_default.headText,
2220
2234
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2663,6 +2677,8 @@ window.__ModuleLoader__.load({
2663
2677
  disposeForm;
2664
2678
  disposed = false;
2665
2679
  saving = false;
2680
+ /** A save asked for while one was in flight; it runs once the first settles (#1754). */
2681
+ saveQueued = false;
2666
2682
  failed = false;
2667
2683
  failedReason;
2668
2684
  /** @param scope - the bound configuration form for this card's namespace. */
@@ -2738,7 +2754,7 @@ window.__ModuleLoader__.load({
2738
2754
  });
2739
2755
  },
2740
2756
  save: () => {
2741
- this.save();
2757
+ this.requestSave();
2742
2758
  },
2743
2759
  discard: () => {
2744
2760
  if (this.staged.size === 0 && !this.failed) return;
@@ -2765,6 +2781,29 @@ window.__ModuleLoader__.load({
2765
2781
  * the user can correct them instead of retyping.
2766
2782
  * @returns settlement after the mutation and the read-back.
2767
2783
  */
2784
+ /**
2785
+ * Run one save, and re-run it once if another was asked for while this one
2786
+ * was still in flight.
2787
+ *
2788
+ * A save is a Host round trip that also drives the profile reconcile, and
2789
+ * the Host runs that write inside one exclusive transaction. Answering a
2790
+ * press that arrives mid-flight by returning immediately dropped the edit
2791
+ * with no explanation, which is the "the save button stops working after a
2792
+ * few rounds" report (#1754). Serializing instead means a save pressed while
2793
+ * another is still settling runs against the settled state - which is what
2794
+ * the operator meant by pressing it again.
2795
+ * @returns settlement after the mutation and the read-back.
2796
+ */
2797
+ async requestSave() {
2798
+ if (this.saving) {
2799
+ this.saveQueued = true;
2800
+ return;
2801
+ }
2802
+ await this.save();
2803
+ if (!this.saveQueued) return;
2804
+ this.saveQueued = false;
2805
+ await this.requestSave();
2806
+ }
2768
2807
  async save() {
2769
2808
  const plan = this.plan();
2770
2809
  const valid = plan.filter((item) => item.judge !== void 0);
@@ -2978,6 +3017,7 @@ window.__ModuleLoader__.load({
2978
3017
  t,
2979
3018
  titleKey: "settings.title",
2980
3019
  descriptionKey: "settings.description",
3020
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhoneIcon, { size: 18 }),
2981
3021
  defaultOpen: false,
2982
3022
  state,
2983
3023
  onSave: props.save,
@@ -3221,8 +3261,6 @@ window.__ModuleLoader__.load({
3221
3261
  const zh = {
3222
3262
  "entry.label": "远程访问",
3223
3263
  "mobile.whale.open": "打开侧边栏",
3224
- "mobile.composer.pickModel": "选择模型",
3225
- "mobile.composer.pickEffort": "选择推理等级",
3226
3264
  "title": "远程访问",
3227
3265
  "subtitle": "通过手机或另一台电脑配对,远程使用同一份 Web 界面(官方界面 + 移动端适配)",
3228
3266
  "card.title": "设备配对",
@@ -3232,7 +3270,7 @@ window.__ModuleLoader__.load({
3232
3270
  "status.disconnected": "已配对设备离线",
3233
3271
  "status.stopped": "已停止远程访问",
3234
3272
  "status.lanRequired": "此功能需要局域网绑定或公网地址才能使用",
3235
- "status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问设置」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。",
3273
+ "status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。",
3236
3274
  "status.loopbackRequired": "配对面板仅限本机使用",
3237
3275
  "status.loopbackRequiredHint": "请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。",
3238
3276
  "status.unreachable": "无法连接配对服务",
@@ -3288,8 +3326,8 @@ window.__ModuleLoader__.load({
3288
3326
  "relay.registering": "正在把临时隧道同步到固定域名(<id>.dsh-market.com)…",
3289
3327
  "relay.failed": "固定域名同步失败:{error}。正在自动重试,期间二维码可能使用临时地址。",
3290
3328
  "close.label": "关闭远程访问面板",
3291
- "settings.title": "远程访问设置",
3292
- "settings.description": "配对安全与设备限额。",
3329
+ "settings.title": "远程访问",
3330
+ "settings.description": "手机配对、公网隧道与设备限额。",
3293
3331
  "settings.enabled": "启用远程访问",
3294
3332
  "settings.enabledHint": "关闭后移除侧边栏入口并停用配对路由与局域网栅栏。",
3295
3333
  "settings.tokenTtlMs": "配对令牌有效期(毫秒)",
@@ -3343,8 +3381,6 @@ window.__ModuleLoader__.load({
3343
3381
  const en = {
3344
3382
  "entry.label": "Remote access",
3345
3383
  "mobile.whale.open": "Open sidebar",
3346
- "mobile.composer.pickModel": "Pick model",
3347
- "mobile.composer.pickEffort": "Pick reasoning effort",
3348
3384
  "title": "Remote access",
3349
3385
  "subtitle": "Pair a phone or another computer to share the same Web GUI (official UI + mobile adaptation)",
3350
3386
  "card.title": "Pair a device",
@@ -3410,8 +3446,8 @@ window.__ModuleLoader__.load({
3410
3446
  "relay.registering": "Syncing the ephemeral tunnel to the stable hostname (<id>.dsh-market.com)…",
3411
3447
  "relay.failed": "Stable-hostname sync failed: {error}. Retrying automatically; the QR may use the ephemeral address meanwhile.",
3412
3448
  "close.label": "Close remote access panel",
3413
- "settings.title": "Remote access settings",
3414
- "settings.description": "Pairing security and device limits.",
3449
+ "settings.title": "Remote access",
3450
+ "settings.description": "Phone pairing, public tunnel and device limits.",
3415
3451
  "settings.enabled": "Enable remote access",
3416
3452
  "settings.enabledHint": "When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.",
3417
3453
  "settings.tokenTtlMs": "Pairing token lifetime (ms)",
@@ -4081,7 +4117,7 @@ window.__ModuleLoader__.load({
4081
4117
  /** The building package's version, when the bundle carries it. */
4082
4118
  function bakedVersion() {
4083
4119
  try {
4084
- return "0.4.3";
4120
+ return "0.4.5";
4085
4121
  } catch {
4086
4122
  return;
4087
4123
  }
@@ -4155,14 +4191,18 @@ window.__ModuleLoader__.load({
4155
4191
  const ACTIVE_CLASS = "dsh-remote-portrait";
4156
4192
  /** Body class while the collapsed rail is hidden behind the whale. */
4157
4193
  const RAIL_HIDDEN_CLASS = "dsh-remote-rail-hidden";
4194
+ /**
4195
+ * The official sidebar column root. Also the scope of the row sweep: every row
4196
+ * the drag suppression covers is a descendant of it, so the document-wide scan
4197
+ * it replaces only ever returned these.
4198
+ */
4199
+ const SIDEBAR_SELECTOR = "[class*=\"_sidebarCol\"]";
4200
+ /** The official draggable session/project rows (see disableRowDrag). */
4201
+ const ROW_SELECTOR = "[class*=\"_sessionRow\"], [class*=\"_projectRow\"]";
4202
+ /** The injected stylesheet, addressed through the same cached lookup. */
4203
+ const STYLE_SELECTOR = `style[data-plugin-css="${ADAPT_CSS_ID}"]`;
4158
4204
  /** Whale button id. */
4159
4205
  const WHALE_ID = "dshRemoteWhale";
4160
- /** Compact picker: synthesized model button id. */
4161
- const MODEL_BTN_ID = "dshRemoteModelPick";
4162
- /** Compact picker: synthesized effort button id. */
4163
- const EFFORT_BTN_ID = "dshRemoteEffortPick";
4164
- /** Body class while the compact picker buttons are wired. */
4165
- const COMPACT_CLASS = "dsh-remote-compact-picker";
4166
4206
  /** Body class while the header actions are seated in the tabs row. */
4167
4207
  const HEADER_SEATED_CLASS = "dsh-remote-header-seated";
4168
4208
  /**
@@ -4171,16 +4211,6 @@ window.__ModuleLoader__.load({
4171
4211
  * reserving the space with padding.
4172
4212
  */
4173
4213
  const HEADER_RESERVE_VAR = "--dsh-remote-header-actions-reserve";
4174
- /** Locale-dependent fast path for the official picker cells (zh/en). */
4175
- const PICKER_CELL_PATTERN = {
4176
- model: /模型|Model/,
4177
- effort: /推理等级|Reasoning|Effort/i
4178
- };
4179
- /** Position of each drill cell among the sheet's chevron cells. */
4180
- const DRILL_INDEX = {
4181
- model: 0,
4182
- effort: 1
4183
- };
4184
4214
  /**
4185
4215
  * The official application frame. The layout column classes are the anchor:
4186
4216
  * `_frame` is shared by unrelated official components (the chat turn rail,
@@ -4256,12 +4286,6 @@ window.__ModuleLoader__.load({
4256
4286
  "[class$=\"_composerSeat\"]{transform:none !important}",
4257
4287
  "[class$=\"_composerSeat\"] [class$=\"_menu\"]{position:fixed !important;left:8px !important;right:8px !important;top:auto !important;bottom:calc(8px + env(safe-area-inset-bottom)) !important;width:auto !important;max-width:none !important;max-height:70dvh !important;overflow-y:auto !important;z-index:2147482000}",
4258
4288
  "[class$=\"_composerSeat\"] [class$=\"_menu\"] [class$=\"_cell\"]{height:44px;min-height:44px;font-size:13px}",
4259
- `body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"]){display:none}`,
4260
- `body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"]{flex-basis:auto;position:static;min-height:0;padding:0;width:0}`,
4261
- `body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] > [class$="_root"]:has([class$="_track"]){right:44px}`,
4262
- `#${MODEL_BTN_ID},#${EFFORT_BTN_ID}{width:26px;height:32px;min-width:26px;padding:0;border-radius:9px;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none;margin-left:4px}`,
4263
- `#${MODEL_BTN_ID} svg,#${EFFORT_BTN_ID} svg{width:16px;height:16px;display:block}`,
4264
- `#${MODEL_BTN_ID}:active,#${EFFORT_BTN_ID}:active{opacity:.7}`,
4265
4289
  "[class$=\"_composerSeat\"] [data-slot=\"conversation.composer.dock\"] [class$=\"_root\"]{font-size:10px;white-space:normal;word-break:break-word;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-align:left;line-height:13px;letter-spacing:-0.2px;padding-left:0;padding-right:0}",
4266
4290
  "[class$=\"_scrollBody\"] [class$=\"_root\"]{font-size:13px}",
4267
4291
  "[class*=\"_bubble\"][role=\"tooltip\"]{display:none}",
@@ -4280,18 +4304,14 @@ window.__ModuleLoader__.load({
4280
4304
  `body.${ACTIVE_CLASS} [data-dsh-plugin=\"pet\"],`,
4281
4305
  `body.${ACTIVE_CLASS} [data-dsh-plugin=\"usage\"]{display:none !important}`,
4282
4306
  `body.${ACTIVE_CLASS} [class$=\"_overlayLayer\"] [class$=\"_workbench\"]{display:none !important}`,
4283
- "[class$=\"_overlay\"] [class$=\"_panel\"]{flex-direction:column;max-height:calc(100dvh - 32px)}",
4284
- "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_nav\"]{flex-direction:row;gap:4px;width:100%;padding:12px 12px 0;overflow-x:auto;overflow-y:hidden}",
4285
- "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navTitle\"]{display:none}",
4286
- "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navList\"]{flex-direction:row;gap:4px}",
4287
- "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navCell\"]{height:34px;padding:0 12px;gap:6px;flex:none;border-radius:10px}",
4288
- "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_navLabel\"]{font-size:13px}",
4289
- "[class$=\"_overlay\"] [class$=\"_panel\"] [class$=\"_content\"]{flex:1;min-height:0}"
4307
+ `body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"]{flex-direction:column;max-height:calc(100dvh - 32px)}`,
4308
+ `body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_nav"]{flex-direction:row;gap:4px;width:100%;padding:12px 12px 0;overflow-x:auto;overflow-y:hidden}`,
4309
+ `body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navTitle"]{display:none}`,
4310
+ `body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navList"]{flex-direction:row;gap:4px}`,
4311
+ `body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navCell"]{height:34px;padding:0 12px;gap:6px;flex:none;border-radius:10px}`,
4312
+ `body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navLabel"]{font-size:13px}`,
4313
+ `body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_content"]{flex:1;min-height:0}`
4290
4314
  ];
4291
- /** Cube glyph for the compact model button (a plain box outline). */
4292
- const CUBE_ICON = "<path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/>";
4293
- /** Level glyph for the compact effort button (three rising bars). */
4294
- const LEVELS_ICON = "<path d=\"M4 6h16\"/><path d=\"M7 12h10\"/><path d=\"M10 18h4\"/>";
4295
4315
  /** The DeepSeek fish glyph (the official brand mark path). */
4296
4316
  const FISH_PATH = "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018Z";
4297
4317
  /**
@@ -4322,6 +4342,8 @@ window.__ModuleLoader__.load({
4322
4342
  let whaleObserver = null;
4323
4343
  /** Header subtree observer: marks the geometry measurement dirty on re-render. */
4324
4344
  let headerObserver = null;
4345
+ /** Document child-list observer: invalidates the cached-absent selector set. */
4346
+ let domObserver = null;
4325
4347
  let observedHeader = null;
4326
4348
  /** Whether the seated-actions geometry needs re-measuring (see alignActionsText). */
4327
4349
  let headerGeometryDirty = true;
@@ -4340,16 +4362,19 @@ window.__ModuleLoader__.load({
4340
4362
  * rail compaction) while the body class stays.
4341
4363
  */
4342
4364
  function ensureAdaptStyle() {
4343
- if (nodeOf(`style[data-plugin-css="${ADAPT_CSS_ID}"]`) !== null) return;
4365
+ if (nodeOf(STYLE_SELECTOR) !== null) return;
4344
4366
  const tag = document.createElement("style");
4345
4367
  tag.dataset.plugin = "remote-web-ui";
4346
4368
  tag.dataset.pluginCss = ADAPT_CSS_ID;
4347
4369
  tag.textContent = ADAPT_CSS.join("");
4348
4370
  document.head.appendChild(tag);
4371
+ nodeCache.set(STYLE_SELECTOR, tag);
4372
+ nodeMissCache.delete(STYLE_SELECTOR);
4349
4373
  }
4350
4374
  function apply() {
4351
4375
  if (active) return;
4352
4376
  active = true;
4377
+ forgetAbsentSelectors();
4353
4378
  document.body.classList.add(ACTIVE_CLASS);
4354
4379
  try {
4355
4380
  w.__dshRemoteAdapt?.closeDetails?.();
@@ -4361,6 +4386,7 @@ window.__ModuleLoader__.load({
4361
4386
  meta.setAttribute("content", `${savedViewportContent}, viewport-fit=cover`);
4362
4387
  }
4363
4388
  ensureWhale();
4389
+ ensureDomObserver();
4364
4390
  syncWhale();
4365
4391
  setWhaleTimer(true);
4366
4392
  seatHeaderActions();
@@ -4370,7 +4396,6 @@ window.__ModuleLoader__.load({
4370
4396
  active = false;
4371
4397
  unseatHeaderActions();
4372
4398
  restoreRowDrag();
4373
- removeCompactPicker();
4374
4399
  document.body.classList.remove(ACTIVE_CLASS);
4375
4400
  document.body.classList.remove(RAIL_HIDDEN_CLASS);
4376
4401
  document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`)?.remove();
@@ -4386,87 +4411,48 @@ window.__ModuleLoader__.load({
4386
4411
  whaleObserver.disconnect();
4387
4412
  whaleObserver = null;
4388
4413
  }
4389
- }
4390
- /** Start/stop the 600ms sync tick; a no-op when already in the asked state. */
4391
- function setWhaleTimer(on) {
4392
- if (on && whaleTimer === null && active) whaleTimer = window.setInterval(syncWhale, 600);
4393
- else if (!on && whaleTimer !== null) {
4394
- window.clearInterval(whaleTimer);
4395
- whaleTimer = null;
4414
+ if (domObserver !== null) {
4415
+ domObserver.disconnect();
4416
+ domObserver = null;
4396
4417
  }
4418
+ forgetAbsentSelectors();
4397
4419
  }
4398
4420
  /**
4399
- * Compact picker (v79): a phone row cannot fit the desktop text triggers,
4400
- * so the model/effort entries become two icon buttons in the trailing
4401
- * row. Both forward to the official picker trigger (its menu renders as
4402
- * the bottom sheet) and then drill straight into the asked cell — model
4403
- * list or effort list — so one tap lands on the list, matching the
4404
- * cube-model / brain-effort mapping. The official context ring next to
4405
- * the send button keeps its own semantics untouched.
4421
+ * Observe the document for the two kinds of change that can make a cached-absent
4422
+ * target discoverable again (see nodeOf): node insertion/removal, and the
4423
+ * class / compat-stamp attributes the cached selectors match on.
4424
+ *
4425
+ * Body class writes are excluded by target: the layer toggles three body
4426
+ * classes every tick, and counting those would invalidate the negative cache
4427
+ * on every tick and restore the very scan this cache removes. They cannot
4428
+ * create a target either — every cached selector matches an element other
4429
+ * than <body>.
4430
+ *
4431
+ * The record queue is drained on a microtask, so this stays off the layout
4432
+ * path; a chat turn's insertions are a handful per second, not per element.
4406
4433
  */
4407
- function removeCompactPicker() {
4408
- document.body.classList.remove(COMPACT_CLASS);
4409
- document.getElementById(MODEL_BTN_ID)?.remove();
4410
- document.getElementById(EFFORT_BTN_ID)?.remove();
4411
- }
4412
- function drillIntoPicker(kind) {
4413
- const trigger = document.querySelector("[class$=\"_composerSeat\"] [class$=\"_trailing\"] [class$=\"_trigger\"]:has([class$=\"_triggerEffort\"])");
4414
- if (trigger === null) return;
4415
- trigger.click();
4416
- let tries = 0;
4417
- const tapCell = () => {
4418
- tries += 1;
4419
- const cells = Array.from(document.querySelectorAll("[class$=\"_composerSeat\"] [class$=\"_menu\"] [class$=\"_cell\"]"));
4420
- const byLabel = cells.find((c) => PICKER_CELL_PATTERN[kind].test(c.textContent ?? ""));
4421
- const drillable = cells.filter((c) => c.querySelector("[class*=\"_cellChevron\"], [class*=\"_chevron\"]"));
4422
- const cell = byLabel ?? drillable[DRILL_INDEX[kind]];
4423
- if (cell !== void 0) {
4424
- cell.click();
4434
+ function ensureDomObserver() {
4435
+ if (domObserver !== null || typeof MutationObserver === "undefined" || !document.body) return;
4436
+ domObserver = new MutationObserver((records) => {
4437
+ for (const record of records) if (record.type === "childList" || record.target !== document.body) {
4438
+ noteDomChange();
4425
4439
  return;
4426
4440
  }
4427
- if (tries < 8) window.setTimeout(tapCell, 150);
4428
- };
4429
- window.setTimeout(tapCell, 150);
4430
- }
4431
- function makeCompactButton(id, title, icon, kind) {
4432
- const btn = document.createElement("button");
4433
- btn.id = id;
4434
- btn.type = "button";
4435
- btn.dataset.dshPlugin = "remote-web-ui";
4436
- btn.title = title;
4437
- btn.setAttribute("aria-label", title);
4438
- btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg>`;
4439
- btn.addEventListener("click", () => {
4440
- drillIntoPicker(kind);
4441
4441
  });
4442
- return btn;
4442
+ domObserver.observe(document.body, {
4443
+ childList: true,
4444
+ subtree: true,
4445
+ attributes: true,
4446
+ attributeFilter: ["class", "data-dsh-frame"]
4447
+ });
4443
4448
  }
4444
- function syncCompactPicker() {
4445
- if (!active) return;
4446
- const tools = nodeOf("[class$=\"_composerSeat\"] [class$=\"_tools\"]");
4447
- const trigger = tools?.parentElement?.querySelector("[class$=\"_triggerEffort\"]")?.parentElement;
4448
- if (tools === null || trigger === null) {
4449
- removeCompactPicker();
4450
- return;
4451
- }
4452
- if (document.getElementById(MODEL_BTN_ID) === null) tools.appendChild(makeCompactButton(MODEL_BTN_ID, surfaceText("mobile.composer.pickModel", "Pick model"), CUBE_ICON, "model"));
4453
- if (document.getElementById(EFFORT_BTN_ID) === null) tools.appendChild(makeCompactButton(EFFORT_BTN_ID, surfaceText("mobile.composer.pickEffort", "Pick reasoning effort"), LEVELS_ICON, "effort"));
4454
- for (const [id, key, fallback] of [[
4455
- MODEL_BTN_ID,
4456
- "mobile.composer.pickModel",
4457
- "Pick model"
4458
- ], [
4459
- EFFORT_BTN_ID,
4460
- "mobile.composer.pickEffort",
4461
- "Pick reasoning effort"
4462
- ]]) {
4463
- const btn = document.getElementById(id);
4464
- if (btn === null) continue;
4465
- const label = surfaceText(key, fallback);
4466
- btn.title = label;
4467
- btn.setAttribute("aria-label", label);
4449
+ /** Start/stop the 600ms sync tick; a no-op when already in the asked state. */
4450
+ function setWhaleTimer(on) {
4451
+ if (on && whaleTimer === null && active) whaleTimer = window.setInterval(syncWhale, 600);
4452
+ else if (!on && whaleTimer !== null) {
4453
+ window.clearInterval(whaleTimer);
4454
+ whaleTimer = null;
4468
4455
  }
4469
- document.body.classList.add(COMPACT_CLASS);
4470
4456
  }
4471
4457
  /**
4472
4458
  * The official sidebar toggle in the logo row. The row's own `_toggle` class
@@ -4634,7 +4620,7 @@ window.__ModuleLoader__.load({
4634
4620
  const dragOverridden = /* @__PURE__ */ new Map();
4635
4621
  function disableRowDrag() {
4636
4622
  if (!active) return;
4637
- const rows = document.querySelectorAll("[class*=\"_sidebarCol\"] [class*=\"_sessionRow\"], [class*=\"_sidebarCol\"] [class*=\"_projectRow\"]");
4623
+ const rows = sidebarRows();
4638
4624
  for (const row of rows) if (row.getAttribute("draggable") !== "false") {
4639
4625
  if (!dragOverridden.has(row)) dragOverridden.set(row, row.getAttribute("draggable"));
4640
4626
  row.setAttribute("draggable", "false");
@@ -4655,14 +4641,83 @@ window.__ModuleLoader__.load({
4655
4641
  * node on a major re-render, which the isConnected guard detects.
4656
4642
  */
4657
4643
  const nodeCache = /* @__PURE__ */ new Map();
4644
+ /**
4645
+ * Selectors known to be absent as of {@link domGeneration}. A miss is the
4646
+ * common case, not the exception: the layer holds no overlay of its own, and
4647
+ * several official surfaces it looks for (tabs row, tools row, sidebar rows)
4648
+ * legitimately do not exist at once. Re-walking a conversation-sized document
4649
+ * for the same absent selector on every 600ms tick was waste that almost
4650
+ * always resolved to null: five document-wide lookups per tick on the measured
4651
+ * fixture, zero after this cache. That is a lookup count — the wall-clock
4652
+ * saving is real while the mounted content is quiet and shrinks to the
4653
+ * row-scope saving when a turn streams (see the Agent Note for the harness and
4654
+ * the before/after numbers).
4655
+ *
4656
+ * A miss is trusted only while the observed content has not changed, so a
4657
+ * surface that appears is still found on the very next tick — the same
4658
+ * discovery latency the unconditional probe had. The cache is never aged by a
4659
+ * clock: it is cleared by {@link noteDomChange} while the observer is live and
4660
+ * by {@link forgetAbsentSelectors} across the windows where it is not (apply
4661
+ * and revert). One blind spot is deliberate and load-bearing: the observer
4662
+ * watches <body>, so a head-resident target is not covered by that signal —
4663
+ * the one such target is seeded into the cache where it is created (see
4664
+ * ensureAdaptStyle).
4665
+ */
4666
+ const nodeMissCache = /* @__PURE__ */ new Set();
4667
+ /**
4668
+ * Bumped whenever a cached target may have appeared or disappeared. A surface
4669
+ * qualifies on a class token or on the aggregate compat stamp
4670
+ * (`data-dsh-frame`), so both insertions/removals AND those two attributes
4671
+ * can change the answer. Nothing the layer writes per tick lands here: its own
4672
+ * class writes are all on <body>, and its row/transform/label writes touch
4673
+ * other attributes entirely (see ensureDomObserver).
4674
+ */
4675
+ let domGeneration = 0;
4676
+ /** The generation the negative cache was recorded against. */
4677
+ let missGeneration = -1;
4678
+ function noteDomChange() {
4679
+ domGeneration += 1;
4680
+ }
4681
+ /**
4682
+ * Drop every cached "absent" verdict. Called on apply and revert: while the
4683
+ * observer is disconnected the layer is blind to insertions, so a verdict
4684
+ * recorded before the gap could otherwise outlive the change that falsified
4685
+ * it and be served without a probe. The positive cache needs no equivalent —
4686
+ * its `isConnected` guard already re-resolves a replaced node.
4687
+ */
4688
+ function forgetAbsentSelectors() {
4689
+ nodeMissCache.clear();
4690
+ domGeneration += 1;
4691
+ }
4658
4692
  function nodeOf(selector) {
4659
4693
  const cached = nodeCache.get(selector);
4660
- if (cached !== void 0 && cached.isConnected) return cached;
4694
+ if (cached !== void 0) {
4695
+ if (cached.isConnected) return cached;
4696
+ nodeCache.delete(selector);
4697
+ }
4698
+ if (missGeneration !== domGeneration) {
4699
+ nodeMissCache.clear();
4700
+ missGeneration = domGeneration;
4701
+ }
4702
+ if (nodeMissCache.has(selector)) return null;
4661
4703
  const found = document.querySelector(selector);
4662
- if (found === null) nodeCache.delete(selector);
4663
- else nodeCache.set(selector, found);
4704
+ if (found === null) {
4705
+ nodeMissCache.add(selector);
4706
+ return null;
4707
+ }
4708
+ nodeCache.set(selector, found);
4664
4709
  return found;
4665
4710
  }
4711
+ /**
4712
+ * The session/project rows the drag suppression covers, scoped to the cached
4713
+ * sidebar root. Each row carries the sidebar column class as an ancestor, so
4714
+ * the document-wide scan this replaces only ever returned rows from here.
4715
+ */
4716
+ function sidebarRows() {
4717
+ const sidebar = nodeOf(SIDEBAR_SELECTOR);
4718
+ if (sidebar === null) return [];
4719
+ return Array.from(sidebar.querySelectorAll(ROW_SELECTOR));
4720
+ }
4666
4721
  /** The official application frame, through the same cached lookup. */
4667
4722
  function frameEl() {
4668
4723
  const frame = nodeOf(APP_FRAME_SELECTOR);
@@ -4688,7 +4743,6 @@ window.__ModuleLoader__.load({
4688
4743
  disableRowDrag();
4689
4744
  seatHeaderActions();
4690
4745
  alignActionsText();
4691
- syncCompactPicker();
4692
4746
  }
4693
4747
  function seatHeaderActions() {
4694
4748
  if (!active) return;