dsh-mobile 0.8.6 → 0.8.8

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.
package/lib/client.js CHANGED
@@ -3813,8 +3813,18 @@ window.__ModuleLoader__.load({
3813
3813
  return;
3814
3814
  }
3815
3815
  }
3816
- /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */
3817
- function parseTaskNotifyPayload(value) {
3816
+ /** Resolve the opaque Session id carried by the App's internal notification route. */
3817
+ function sessionIdFromNotificationRoute(route, base = "https://mobile.invalid/") {
3818
+ if (route === null || route === void 0) return void 0;
3819
+ let encoded;
3820
+ try {
3821
+ encoded = new URL(route, base).searchParams.get("dsh-session");
3822
+ } catch {
3823
+ return;
3824
+ }
3825
+ return decodeTaskNotificationNavigate(encoded);
3826
+ }
3827
+ /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */ function parseTaskNotifyPayload(value) {
3818
3828
  let record;
3819
3829
  if (typeof value === "string") try {
3820
3830
  const parsed = JSON.parse(value);
@@ -3854,22 +3864,46 @@ window.__ModuleLoader__.load({
3854
3864
  return true;
3855
3865
  }
3856
3866
  /**
3867
+ * Whether the App's background service reports that it will actually post a
3868
+ * task-completion notification. All three facts must be explicit: the service
3869
+ * is enabled, its event stream is connected, and Android currently allows the
3870
+ * task channel to post (`canPost`: notifications enabled and channel importance
3871
+ * not NONE). The App records `notify:blocked` and does not touch
3872
+ * `connected`/`lastEventAt` when `canPost` is false, so treating enabled+
3873
+ * connected alone as a promise would let the page yield to a silent App and
3874
+ * drop the notification entirely.
3875
+ */
3876
+ function backgroundNotificationsWillPost(report) {
3877
+ if (report === null || typeof report !== "object" || Array.isArray(report)) return false;
3878
+ const record = report;
3879
+ return record.enabled === true && record.connected === true && record.canPost === true;
3880
+ }
3881
+ /**
3857
3882
  * Deliver one notification through the native bridge when available.
3858
3883
  * Returns false when there is no bridge (desktop browsers keep their own
3859
3884
  * UI) so callers can fall back; a denial flips the module sticky flag via
3860
3885
  * onDenied for the session-lifetime nag guard.
3886
+ *
3887
+ * For task-completion notifications the page first asks the App whether its
3888
+ * background service will post the same event. When it will, the page yields:
3889
+ * one completed turn then produces exactly one notification, authored by the
3890
+ * plugin's Host-side event text and rendered by the App. Old Apps (no
3891
+ * `notification.background.get` report, no `connected` state, or no `canPost`
3892
+ * state) keep the previous behavior and the page still notifies.
3861
3893
  */
3862
3894
  function fireTaskNotifyEvent(event, onDenied) {
3863
3895
  const bridge = readNativeBridge();
3864
3896
  if (bridge === void 0) return false;
3897
+ const deliver = () => bridge.invoke("notification.notify", {
3898
+ title: event.title,
3899
+ body: event.body,
3900
+ tag: event.tag,
3901
+ ...event.navigate === void 0 ? {} : { navigate: event.navigate }
3902
+ });
3865
3903
  Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
3866
3904
  if (!capabilities.includes("notification.notify")) return;
3867
- return bridge.invoke("notification.notify", {
3868
- title: event.title,
3869
- body: event.body,
3870
- tag: event.tag,
3871
- ...event.navigate === void 0 ? {} : { navigate: event.navigate }
3872
- });
3905
+ if (event.kind !== "done" || !capabilities.includes("notification.background.get")) return deliver();
3906
+ return bridge.invoke("notification.background.get", {}).then((report) => backgroundNotificationsWillPost(report) ? void 0 : deliver(), () => deliver());
3873
3907
  }).then(() => void 0, (error) => {
3874
3908
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
3875
3909
  if ((code === "permission_denied" || code === "denied") && onDenied !== void 0) onDenied();
@@ -4306,6 +4340,13 @@ window.__ModuleLoader__.load({
4306
4340
  .dsh-mobile-settings_selector:hover:not(:disabled) { background:var(--dsw-alias-interactive-bg-hover); }
4307
4341
  .dsh-mobile-settings_selector:active:not(:disabled) { background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover)); }
4308
4342
  .dsh-mobile-settings_selector:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
4343
+ /* Row switches (session prefetch, composer collapse) pair a visually-hidden
4344
+ native checkbox with an On/Off pill. Hide the checkbox so the pill is the only
4345
+ visible control, while the label keeps the 48px row a tap target and the input
4346
+ stays keyboard-focusable; the pill's :focus-visible ring is driven by the
4347
+ checkbox's focus via :has. */
4348
+ .dsh-mobile-settings_switchInput { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0; opacity:0; }
4349
+ .dsh-mobile-settings_row:has(.dsh-mobile-settings_switchInput:focus-visible) .dsh-mobile-settings_selector { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
4309
4350
  .dsh-mobile-settings_selector:disabled { cursor:wait; opacity:.55; }
4310
4351
  .dsh-mobile-settings_fontControls { display:flex; align-items:center; gap:8px; flex-shrink:0; }
4311
4352
  .dsh-mobile-settings_fontControls button { min-width:48px; padding:0 12px; }
@@ -7098,6 +7139,124 @@ window.__ModuleLoader__.load({
7098
7139
  }
7099
7140
  }, copy.reset)));
7100
7141
  }
7142
+ // src/composer-collapse.ts
7143
+ /**
7144
+ * Storage key for the opt-in switch.
7145
+ *
7146
+ * The setting row lives in the client bundle while the tracker lives in the
7147
+ * layout bundle, and tsdown.config.ts emits those as two independent CJS modules
7148
+ * that each inline this file. A module-level singleton therefore cannot carry the
7149
+ * value across: the row would flip its own copy while the layout kept the
7150
+ * boot-time value, and the toggle would only apply after a reload. The persisted
7151
+ * flag plus the two events below are the shared state instead, mirroring the
7152
+ * session-prefetch opt-in.
7153
+ */
7154
+ const COMPOSER_COLLAPSE_STORAGE_KEY = "dsh-mobile.composer-collapse.enabled";
7155
+ const COMPOSER_COLLAPSE_EVENT = "dsh-mobile:composer-collapse-change";
7156
+ /**
7157
+ * Last value written in this page when storage refused the write.
7158
+ *
7159
+ * This is a per-bundle fallback for the blocked-storage case only: it is not the
7160
+ * cross-bundle transport (that is the persisted flag plus the events above). It
7161
+ * exists so a switch that cannot be persisted still reads back as flipped in the
7162
+ * same page, instead of the row snapping back while the layout obeys the event.
7163
+ */
7164
+ let unpersistedEnabled;
7165
+ /** Read the persisted opt-in. Defaults to off; unreadable storage keeps it off. */
7166
+ function readComposerCollapseEnabled(storage) {
7167
+ try {
7168
+ const stored = (storage === void 0 ? globalThis.localStorage : storage)?.getItem("dsh-mobile.composer-collapse.enabled") ?? null;
7169
+ if (stored !== null) return stored === "true";
7170
+ return storage === void 0 ? unpersistedEnabled ?? false : false;
7171
+ } catch {
7172
+ return storage === void 0 ? unpersistedEnabled ?? false : false;
7173
+ }
7174
+ }
7175
+ /** Persist the opt-in and notify the layout bundle in the same page. */
7176
+ function writeComposerCollapseEnabled(enabled, storage) {
7177
+ try {
7178
+ const resolved = storage === void 0 ? globalThis.localStorage : storage;
7179
+ if (resolved === null) throw new Error("storage unavailable");
7180
+ resolved.setItem(COMPOSER_COLLAPSE_STORAGE_KEY, String(enabled));
7181
+ if (storage === void 0) unpersistedEnabled = void 0;
7182
+ } catch {
7183
+ if (storage === void 0) unpersistedEnabled = enabled;
7184
+ }
7185
+ try {
7186
+ globalThis.dispatchEvent?.(new CustomEvent(COMPOSER_COLLAPSE_EVENT, { detail: { enabled } }));
7187
+ } catch {}
7188
+ }
7189
+ /**
7190
+ * Subscribe to opt-in changes coming from either bundle. The listener is not
7191
+ * called with the initial value; read it once with readComposerCollapseEnabled().
7192
+ */
7193
+ function subscribeComposerCollapseEnabled(listener) {
7194
+ const onStorage = (event) => {
7195
+ if (event.key !== null && event.key !== "dsh-mobile.composer-collapse.enabled") return;
7196
+ listener(readComposerCollapseEnabled());
7197
+ };
7198
+ const onCustom = (event) => {
7199
+ const detail = event.detail;
7200
+ listener(detail?.enabled ?? readComposerCollapseEnabled());
7201
+ };
7202
+ globalThis.addEventListener?.("storage", onStorage);
7203
+ globalThis.addEventListener?.(COMPOSER_COLLAPSE_EVENT, onCustom);
7204
+ return () => {
7205
+ globalThis.removeEventListener?.("storage", onStorage);
7206
+ globalThis.removeEventListener?.(COMPOSER_COLLAPSE_EVENT, onCustom);
7207
+ };
7208
+ }
7209
+ // src/composer-collapse-settings.ts
7210
+ const COMPOSER_COLLAPSE_COPY = {
7211
+ en: {
7212
+ title: "Reading mode: collapse composer",
7213
+ desc: "Collapse the composer editor while scrolling down a long conversation; tap it, focus it, or scroll back up to restore. It never collapses while a draft or a running reply is present.",
7214
+ on: "On",
7215
+ off: "Off"
7216
+ },
7217
+ zh: {
7218
+ title: "阅读模式:收起输入栏",
7219
+ desc: "向下滚动阅读长会话时收起输入框编辑区;点按、聚焦或向上回滚即恢复。有草稿或回复生成中时不会收起。",
7220
+ on: "已开启",
7221
+ off: "已关闭"
7222
+ },
7223
+ it: {
7224
+ title: "Modalità lettura: comprimi il campo",
7225
+ desc: "Comprime l’editor del campo mentre scorri una conversazione lunga; toccalo, mettilo a fuoco o scorri verso l’alto per ripristinarlo. Non si comprime con una bozza o una risposta in corso.",
7226
+ on: "Attivo",
7227
+ off: "Disattivo"
7228
+ }
7229
+ };
7230
+ /** Browser-only settings row; toggling it only persists the flag. */
7231
+ function ComposerCollapseSettings({ locale, read, write, subscribe }) {
7232
+ const readEnabled = read ?? (() => readComposerCollapseEnabled());
7233
+ const writeEnabled = write ?? ((enabled) => {
7234
+ writeComposerCollapseEnabled(enabled);
7235
+ });
7236
+ const enabled = (0, react.useSyncExternalStore)(subscribe ?? ((listener) => subscribeComposerCollapseEnabled(listener)), readEnabled, () => false);
7237
+ const copy = COMPOSER_COLLAPSE_COPY[locale];
7238
+ return (0, react.createElement)("label", {
7239
+ className: "dsh-mobile-settings_row",
7240
+ lang: locale === "zh" ? "zh-CN" : locale,
7241
+ "data-dsh-mobile-composer-collapse": enabled ? "on" : "off"
7242
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
7243
+ className: "dsh-mobile-settings_desc",
7244
+ "aria-live": "polite"
7245
+ }, copy.desc)), (0, react.createElement)("input", {
7246
+ type: "checkbox",
7247
+ role: "switch",
7248
+ className: "dsh-mobile-settings_switchInput",
7249
+ checked: enabled,
7250
+ "aria-label": copy.title,
7251
+ "aria-checked": enabled ? "true" : "false",
7252
+ onChange: (event) => {
7253
+ writeEnabled(event.currentTarget.checked);
7254
+ }
7255
+ }), (0, react.createElement)("span", {
7256
+ className: "dsh-mobile-settings_selector",
7257
+ "aria-hidden": "true"
7258
+ }, enabled ? copy.on : copy.off));
7259
+ }
7101
7260
  // src/mobile-display-scale.ts
7102
7261
  /** Native App page scaling in DSH General settings, independent from local typography. */
7103
7262
  /** Validate the native reply before displaying or reusing its values in a request. */
@@ -9945,6 +10104,104 @@ window.__ModuleLoader__.load({
9945
10104
  disabled: true
9946
10105
  }, copy.busy)));
9947
10106
  }
10107
+ // src/session-prefetch.ts
10108
+ /**
10109
+ * Storage key for the opt-in switch.
10110
+ *
10111
+ * The setting row lives in the client bundle while the tracker lives in the
10112
+ * layout bundle (tsdown.config.ts emits them separately and inlines this module
10113
+ * into each), so a module-level singleton cannot carry the value across. The
10114
+ * layout bundle owns the persisted state instead; the row notifies it through
10115
+ * both the storage event (other tabs) and a custom event (same page).
10116
+ */
10117
+ const SESSION_PREFETCH_STORAGE_KEY = "dsh-mobile.session-prefetch.enabled";
10118
+ const SESSION_PREFETCH_EVENT = "dsh-mobile:session-prefetch-change";
10119
+ /** Read the persisted opt-in. Defaults to off; unreadable storage keeps it off. */
10120
+ function readSessionPrefetchEnabled(storage) {
10121
+ try {
10122
+ return (storage ?? globalThis.localStorage)?.getItem(SESSION_PREFETCH_STORAGE_KEY) === "true";
10123
+ } catch {
10124
+ return false;
10125
+ }
10126
+ }
10127
+ /** Persist the opt-in and notify the layout bundle in the same page. */
10128
+ function writeSessionPrefetchEnabled(enabled, storage) {
10129
+ try {
10130
+ (storage ?? globalThis.localStorage)?.setItem(SESSION_PREFETCH_STORAGE_KEY, String(enabled));
10131
+ } catch {}
10132
+ try {
10133
+ globalThis.dispatchEvent?.(new CustomEvent(SESSION_PREFETCH_EVENT, { detail: { enabled } }));
10134
+ } catch {}
10135
+ }
10136
+ /**
10137
+ * Subscribe to opt-in changes coming from either bundle. The listener is not
10138
+ * called with the initial value; read it once with readSessionPrefetchEnabled().
10139
+ */
10140
+ function subscribeSessionPrefetchEnabled(listener) {
10141
+ const onStorage = (event) => {
10142
+ if (event.key !== null && event.key !== "dsh-mobile.session-prefetch.enabled") return;
10143
+ listener(readSessionPrefetchEnabled());
10144
+ };
10145
+ const onCustom = (event) => {
10146
+ const detail = event.detail;
10147
+ listener(detail?.enabled ?? readSessionPrefetchEnabled());
10148
+ };
10149
+ globalThis.addEventListener?.("storage", onStorage);
10150
+ globalThis.addEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
10151
+ return () => {
10152
+ globalThis.removeEventListener?.("storage", onStorage);
10153
+ globalThis.removeEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
10154
+ };
10155
+ }
10156
+ // src/session-prefetch-settings.ts
10157
+ const SESSION_PREFETCH_COPY = {
10158
+ en: {
10159
+ title: "Prefetch the session you just left",
10160
+ desc: "After switching sessions, warm the previous one in the background so switching back opens faster. Read-only, at most one session per switch, with a per-session cooldown.",
10161
+ on: "On",
10162
+ off: "Off"
10163
+ },
10164
+ zh: {
10165
+ title: "预取刚离开的会话",
10166
+ desc: "切换会话后在后台预热上一个会话,切回去时打开更快。只读、每次切换最多预热一个会话,并带单会话冷却。",
10167
+ on: "已开启",
10168
+ off: "已关闭"
10169
+ },
10170
+ it: {
10171
+ title: "Prefetch della sessione appena lasciata",
10172
+ desc: "Dopo un cambio di sessione riscalda in background quella precedente, così tornare indietro è più rapido. Sola lettura, al massimo una sessione per cambio, con cooldown per sessione.",
10173
+ on: "Attivo",
10174
+ off: "Disattivo"
10175
+ }
10176
+ };
10177
+ /** Browser-only settings row; toggling it only persists the flag. */
10178
+ function SessionPrefetchSettings({ locale, read, write, subscribe }) {
10179
+ const readEnabled = read ?? readSessionPrefetchEnabled;
10180
+ const writeEnabled = write ?? writeSessionPrefetchEnabled;
10181
+ const enabled = (0, react.useSyncExternalStore)(subscribe ?? subscribeSessionPrefetchEnabled, readEnabled, () => false);
10182
+ const copy = SESSION_PREFETCH_COPY[locale];
10183
+ return (0, react.createElement)("label", {
10184
+ className: "dsh-mobile-settings_row",
10185
+ lang: locale === "zh" ? "zh-CN" : locale,
10186
+ "data-dsh-mobile-session-prefetch": enabled ? "on" : "off"
10187
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
10188
+ className: "dsh-mobile-settings_desc",
10189
+ "aria-live": "polite"
10190
+ }, copy.desc)), (0, react.createElement)("input", {
10191
+ type: "checkbox",
10192
+ role: "switch",
10193
+ className: "dsh-mobile-settings_switchInput",
10194
+ checked: enabled,
10195
+ "aria-label": copy.title,
10196
+ "aria-checked": enabled ? "true" : "false",
10197
+ onChange: (event) => {
10198
+ writeEnabled(event.currentTarget.checked);
10199
+ }
10200
+ }), (0, react.createElement)("span", {
10201
+ className: "dsh-mobile-settings_selector",
10202
+ "aria-hidden": "true"
10203
+ }, enabled ? copy.on : copy.off));
10204
+ }
9948
10205
  // src/mobile-push/navigation.ts
9949
10206
  /**
9950
10207
  * Consume one authenticated tap reference after the public Workspace and Session
@@ -11458,7 +11715,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11458
11715
  enabled: "关闭后台提醒",
11459
11716
  disabled: "开启后台提醒",
11460
11717
  busy: "正在处理…",
11461
- error: "无法更新后台提醒;请确认任务通知权限和当前连接。"
11718
+ error: "无法更新后台提醒;请确认任务通知权限和当前连接。",
11719
+ connected: "已连接",
11720
+ disconnected: "未连接"
11462
11721
  };
11463
11722
  if (locale === "it") return {
11464
11723
  title: "Promemoria in background",
@@ -11466,7 +11725,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11466
11725
  enabled: "Disattiva promemoria",
11467
11726
  disabled: "Attiva promemoria",
11468
11727
  busy: "Operazione…",
11469
- error: "Impossibile aggiornare i promemoria; controlla il permesso notifiche e la connessione attiva."
11728
+ error: "Impossibile aggiornare i promemoria; controlla il permesso notifiche e la connessione attiva.",
11729
+ connected: "Connesso",
11730
+ disconnected: "Non connesso"
11470
11731
  };
11471
11732
  return {
11472
11733
  title: "Background task reminders",
@@ -11474,13 +11735,24 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11474
11735
  enabled: "Disable background reminders",
11475
11736
  disabled: "Enable background reminders",
11476
11737
  busy: "Working…",
11477
- error: "Could not update background reminders. Check notification permission and the active connection."
11738
+ error: "Could not update background reminders. Check notification permission and the active connection.",
11739
+ connected: "Connected",
11740
+ disconnected: "Not connected"
11478
11741
  };
11479
11742
  }
11743
+ /** Render the App's real connection state when the installed App reports it. */
11744
+ function mobileBackgroundStatusText(locale, connected, lastError) {
11745
+ if (connected === void 0) return void 0;
11746
+ const copy = mobileBackgroundNotificationCopy(locale);
11747
+ if (connected) return copy.connected;
11748
+ return lastError === void 0 ? copy.disconnected : `${copy.disconnected} (${lastError})`;
11749
+ }
11480
11750
  /** Android-only opt-in for the foreground SSE fallback when the WebView is suspended. */
11481
11751
  function MobileBackgroundNotificationRow() {
11482
11752
  const [supported, setSupported] = (0, react.useState)(false);
11483
11753
  const [enabled, setEnabled] = (0, react.useState)(false);
11754
+ const [connected, setConnected] = (0, react.useState)(void 0);
11755
+ const [lastError, setLastError] = (0, react.useState)(void 0);
11484
11756
  const [busy, setBusy] = (0, react.useState)(false);
11485
11757
  const [error, setError] = (0, react.useState)(false);
11486
11758
  const generation = (0, react.useRef)(0);
@@ -11505,6 +11777,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11505
11777
  if (!active || attempt !== generation.current) return;
11506
11778
  if (typeof result?.enabled !== "boolean") throw new Error("invalid background notification status");
11507
11779
  setEnabled(result.enabled);
11780
+ setConnected(typeof result.connected === "boolean" ? result.connected : void 0);
11781
+ setLastError(typeof result.lastError === "string" ? result.lastError : void 0);
11508
11782
  setError(false);
11509
11783
  }).catch(() => {
11510
11784
  if (active && attempt === generation.current) {
@@ -11524,6 +11798,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11524
11798
  if (!supported) return null;
11525
11799
  const locale = selectedMobileControlLocale();
11526
11800
  const copy = mobileBackgroundNotificationCopy(locale);
11801
+ const statusText = enabled ? mobileBackgroundStatusText(locale, connected, lastError) : void 0;
11527
11802
  const toggle = () => {
11528
11803
  const bridge = window.__DSH_MOBILE_NATIVE__;
11529
11804
  if (bridge === void 0 || busy) return;
@@ -11535,6 +11810,13 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11535
11810
  const next = result?.enabled;
11536
11811
  if (typeof next !== "boolean") throw new Error("invalid background notification status");
11537
11812
  setEnabled(next);
11813
+ bridge.invoke("notification.background.get", {}).then((statusResult) => {
11814
+ if (attempt !== generation.current) return;
11815
+ const status = statusResult;
11816
+ if (typeof status?.enabled === "boolean") setEnabled(status.enabled);
11817
+ setConnected(typeof status?.connected === "boolean" ? status.connected : void 0);
11818
+ setLastError(typeof status?.lastError === "string" ? status.lastError : void 0);
11819
+ }).catch(() => void 0);
11538
11820
  }).catch(() => {
11539
11821
  if (attempt === generation.current) setError(true);
11540
11822
  }).finally(() => {
@@ -11547,7 +11829,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11547
11829
  }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
11548
11830
  className: "dsh-mobile-settings_desc",
11549
11831
  "aria-live": "polite"
11550
- }, error ? copy.error : copy.description)), (0, react.createElement)("button", {
11832
+ }, error ? copy.error : statusText ?? copy.description)), (0, react.createElement)("button", {
11551
11833
  type: "button",
11552
11834
  className: "dsh-mobile-settings_selector",
11553
11835
  disabled: busy,
@@ -17656,6 +17938,45 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17656
17938
  fiber.dispose();
17657
17939
  };
17658
17940
  }
17941
+ /**
17942
+ * Open a Session in the live page from an Android notification tap. The App calls this through
17943
+ * `window.__dshMobileOpenSession` instead of reloading the document; returning false asks the App
17944
+ * to fall back to a route navigation (for example while the page is still booting).
17945
+ */
17946
+ function installNativeNotificationNavigation(ctx, view) {
17947
+ if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated" || ctx.inject === void 0) return () => void 0;
17948
+ let closed = false;
17949
+ const target = view;
17950
+ const fiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
17951
+ if (closed) return () => void 0;
17952
+ const context = navigationContext;
17953
+ const open = (route) => {
17954
+ if (closed || typeof route !== "string") return false;
17955
+ const sessionId = sessionIdFromNotificationRoute(route);
17956
+ if (sessionId === void 0) return false;
17957
+ const workspace = context.uiWorkspace;
17958
+ if (workspace === void 0 || typeof workspace.openSession !== "function") return false;
17959
+ const sessions = context.sessions?.list.getSnapshot();
17960
+ if (sessions !== void 0 && sessions.phase === "ready" && !sessions.ids.includes(sessionId)) return false;
17961
+ try {
17962
+ workspace.openSession(sessionId);
17963
+ return true;
17964
+ } catch {
17965
+ return false;
17966
+ }
17967
+ };
17968
+ target.__dshMobileOpenSession = open;
17969
+ return () => {
17970
+ if (target.__dshMobileOpenSession === open) delete target.__dshMobileOpenSession;
17971
+ };
17972
+ });
17973
+ return () => {
17974
+ if (closed) return;
17975
+ closed = true;
17976
+ delete target.__dshMobileOpenSession;
17977
+ fiber.dispose();
17978
+ };
17979
+ }
17659
17980
  /** Own the browser-only push controller, settings row, and optional authenticated tap navigation. */
17660
17981
  function installMobilePushBrowserClient(ctx, runtime) {
17661
17982
  const { page, view } = runtime;
@@ -17729,6 +18050,19 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17729
18050
  controller.stop();
17730
18051
  };
17731
18052
  }
18053
+ /**
18054
+ * Register the session-prefetch opt-in on every dedicated Mobile page, including the Android
18055
+ * App webview. The row used to live inside the browser-push installer, which returns early when
18056
+ * the native bridge exists, so App users never saw the switch for a layout-side feature they use.
18057
+ */
18058
+ function installSessionPrefetchSettings(ctx, view) {
18059
+ if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated") return () => void 0;
18060
+ return ctx.slots.inject("settings.general.item", () => ctx.slots.register({
18061
+ name: "settings.general.item",
18062
+ id: "dsh-mobile-session-prefetch",
18063
+ order: 130
18064
+ }, () => (0, react.createElement)(SessionPrefetchSettings, { locale: selectedMobileControlLocale() })));
18065
+ }
17732
18066
  /** Mount the desktop control or mobile feature enhancements. */
17733
18067
  function apply(ctx) {
17734
18068
  let gatewayRejectionSink;
@@ -17768,6 +18102,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17768
18102
  view: window
17769
18103
  }), "dsh-mobile: authenticated browser push lifecycle");
17770
18104
  ctx.effect(() => installTaskNotificationNavigation(ctx, document, window), "dsh-mobile: native task notification navigation");
18105
+ ctx.effect(() => installNativeNotificationNavigation(ctx, window), "dsh-mobile: in-page notification navigation");
18106
+ ctx.effect(() => installSessionPrefetchSettings(ctx, window), "dsh-mobile: session prefetch settings");
17771
18107
  ctx.effect(() => {
17772
18108
  const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
17773
18109
  const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
@@ -17810,6 +18146,11 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17810
18146
  preference: localFont.preference,
17811
18147
  locale: selectedMobileControlLocale()
17812
18148
  }));
18149
+ const removeComposerCollapse = ctx.slots.register({
18150
+ name: "settings.general.item",
18151
+ id: "dsh-mobile-composer-collapse",
18152
+ order: 15
18153
+ }, () => (0, react.createElement)(ComposerCollapseSettings, { locale: selectedMobileControlLocale() }));
17813
18154
  const removeDisplayScale = ctx.slots.register({
17814
18155
  name: "settings.general.item",
17815
18156
  id: "dsh-mobile-display-scale",
@@ -17835,6 +18176,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17835
18176
  removeTaskNotifications();
17836
18177
  removeSwitchComputer();
17837
18178
  removeDisplayScale();
18179
+ removeComposerCollapse();
17838
18180
  removeFontSize();
17839
18181
  };
17840
18182
  });
@@ -17944,15 +18286,19 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17944
18286
  exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
17945
18287
  exports.installMobileNativeBack = installMobileNativeBack;
17946
18288
  exports.installMobilePushBrowserClient = installMobilePushBrowserClient;
18289
+ exports.installNativeNotificationNavigation = installNativeNotificationNavigation;
18290
+ exports.installSessionPrefetchSettings = installSessionPrefetchSettings;
17947
18291
  exports.installTaskNotificationNavigation = installTaskNotificationNavigation;
17948
18292
  exports.localizedDiagnosticEntryCopy = localizedDiagnosticEntryCopy;
17949
18293
  exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
18294
+ exports.mobileBackgroundStatusText = mobileBackgroundStatusText;
17950
18295
  exports.mobilePushNavigationNotice = mobilePushNavigationNotice;
17951
18296
  exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
17952
18297
  exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
17953
18298
  exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
17954
18299
  exports.parseMobileRemoteProvider = parseMobileRemoteProvider;
17955
18300
  exports.publishAuthoritativeExtensionIds = publishAuthoritativeExtensionIds;
18301
+ exports.readComposerCollapseEnabled = readComposerCollapseEnabled;
17956
18302
  exports.reconcileRemovedExtensions = reconcileRemovedExtensions;
17957
18303
  exports.registerUniqueDisposable = registerUniqueDisposable;
17958
18304
  exports.renderDiagnosticPayloadSafely = renderDiagnosticPayloadSafely;
@@ -17965,6 +18311,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17965
18311
  exports.startLifecycleRefreshScheduler = startLifecycleRefreshScheduler;
17966
18312
  exports.trustAuthenticatedGatewayConnection = trustAuthenticatedGatewayConnection;
17967
18313
  exports.validateDiagnosticChecks = validateDiagnosticChecks;
18314
+ exports.writeComposerCollapseEnabled = writeComposerCollapseEnabled;
17968
18315
  return module.exports;
17969
18316
  }
17970
18317
  });