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.
@@ -24,19 +24,438 @@ window.__ModuleLoader__.load({
24
24
  it: Object.freeze({
25
25
  closePanels: "Chiudi pannelli",
26
26
  workspaceNavigation: "Navigazione area di lavoro e sessioni",
27
- backToConversation: "Torna alla conversazione"
27
+ backToConversation: "Torna alla conversazione",
28
+ composerRestore: "Mostra il campo di scrittura"
28
29
  }),
29
30
  en: Object.freeze({
30
31
  closePanels: "Close panels",
31
32
  workspaceNavigation: "Workspace and session navigation",
32
- backToConversation: "Back to conversation"
33
+ backToConversation: "Back to conversation",
34
+ composerRestore: "Show composer"
33
35
  }),
34
36
  zh: Object.freeze({
35
37
  closePanels: "关闭浮层",
36
38
  workspaceNavigation: "工作区与会话导航",
37
- backToConversation: "返回会话"
39
+ backToConversation: "返回会话",
40
+ composerRestore: "展开输入框"
38
41
  })
39
42
  });
43
+ // src/composer-collapse.ts
44
+ const COMPOSER_COLLAPSE_DEFAULTS = {
45
+ enabled: false,
46
+ collapseThresholdPx: 480,
47
+ reverseResetPx: 80
48
+ };
49
+ /**
50
+ * Track cumulative reading progress across scroll events. This is the only
51
+ * collapse policy: a decision table plus accumulated progress, kept out of the
52
+ * component so it stays testable without a DOM.
53
+ */
54
+ var ComposerCollapseTracker = class {
55
+ readingProgress = 0;
56
+ reverseProgress = 0;
57
+ state = "expanded";
58
+ config;
59
+ constructor(config) {
60
+ this.config = config;
61
+ }
62
+ /** Whether the opt-in switch is currently on. */
63
+ get enabled() {
64
+ return this.config.enabled;
65
+ }
66
+ /** Update whether collapse is enabled (the user's settings toggle). */
67
+ setEnabled(enabled) {
68
+ this.config = {
69
+ ...this.config,
70
+ enabled
71
+ };
72
+ if (!enabled) this.reset();
73
+ }
74
+ /**
75
+ * Feed one scroll sample; returns the resulting presentation state.
76
+ *
77
+ * reverseResetPx is a deliberate dead zone: a collapsed composer stays
78
+ * collapsed until the user scrolls back up at least that far, so a 1px jitter
79
+ * does not flip the layout on every frame.
80
+ */
81
+ sample(deltaY) {
82
+ if (!this.config.enabled) {
83
+ this.reset();
84
+ return this.state;
85
+ }
86
+ if (this.state === "collapsed") {
87
+ if (deltaY > 0) {
88
+ this.reverseProgress = 0;
89
+ return this.state;
90
+ }
91
+ this.reverseProgress += -deltaY;
92
+ if (this.reverseProgress >= Math.max(this.config.reverseResetPx, 1)) this.reset();
93
+ return this.state;
94
+ }
95
+ if (deltaY < 0) {
96
+ this.readingProgress = Math.max(0, this.readingProgress + deltaY);
97
+ this.reverseProgress = 0;
98
+ return this.state;
99
+ }
100
+ this.readingProgress += deltaY;
101
+ this.reverseProgress = 0;
102
+ if (this.readingProgress >= this.config.collapseThresholdPx) this.state = "collapsed";
103
+ return this.state;
104
+ }
105
+ /** User interacted with the composer (focus, tap, keyboard, draft): expand. */
106
+ expand() {
107
+ this.reset();
108
+ }
109
+ get stateValue() {
110
+ return this.state;
111
+ }
112
+ /** @internal exposed for tests */
113
+ progress() {
114
+ return this.readingProgress;
115
+ }
116
+ reset() {
117
+ this.state = "expanded";
118
+ this.readingProgress = 0;
119
+ this.reverseProgress = 0;
120
+ }
121
+ };
122
+ const COMPOSER_COLLAPSE_EVENT = "dsh-mobile:composer-collapse-change";
123
+ /** Read the persisted opt-in. Defaults to off; unreadable storage keeps it off. */
124
+ function readComposerCollapseEnabled(storage) {
125
+ try {
126
+ const stored = (storage === void 0 ? globalThis.localStorage : storage)?.getItem("dsh-mobile.composer-collapse.enabled") ?? null;
127
+ if (stored !== null) return stored === "true";
128
+ return storage === void 0 ? false : false;
129
+ } catch {
130
+ return storage === void 0 ? false : false;
131
+ }
132
+ }
133
+ /**
134
+ * Subscribe to opt-in changes coming from either bundle. The listener is not
135
+ * called with the initial value; read it once with readComposerCollapseEnabled().
136
+ */
137
+ function subscribeComposerCollapseEnabled(listener) {
138
+ const onStorage = (event) => {
139
+ if (event.key !== null && event.key !== "dsh-mobile.composer-collapse.enabled") return;
140
+ listener(readComposerCollapseEnabled());
141
+ };
142
+ const onCustom = (event) => {
143
+ const detail = event.detail;
144
+ listener(detail?.enabled ?? readComposerCollapseEnabled());
145
+ };
146
+ globalThis.addEventListener?.("storage", onStorage);
147
+ globalThis.addEventListener?.(COMPOSER_COLLAPSE_EVENT, onCustom);
148
+ return () => {
149
+ globalThis.removeEventListener?.("storage", onStorage);
150
+ globalThis.removeEventListener?.(COMPOSER_COLLAPSE_EVENT, onCustom);
151
+ };
152
+ }
153
+ /** Apply the persisted opt-in onto a base config. */
154
+ function composerCollapseConfigFrom(enabled, base = COMPOSER_COLLAPSE_DEFAULTS) {
155
+ return {
156
+ ...base,
157
+ enabled
158
+ };
159
+ }
160
+ /**
161
+ * 软键盘开合监听。
162
+ * 键盘弹起(visualViewport 缩小)意味着用户要输入,应展开收起态。
163
+ * 封装在这里而不是 layout,是为了让 layout 源码不含 visualViewport——
164
+ * 那里只用 window.innerWidth 判宽屏,否则软键盘缩小可视视口会把手机误判成宽屏。
165
+ */
166
+ function onViewportResize(handler) {
167
+ const viewport = globalThis.visualViewport;
168
+ if (viewport === null || viewport === void 0) return () => {};
169
+ viewport.addEventListener("resize", handler);
170
+ return () => {
171
+ viewport.removeEventListener("resize", handler);
172
+ };
173
+ }
174
+ const COLLAPSED_ATTRIBUTE = "data-dsh-mobile-composer-collapsed";
175
+ const RESTORE_LABEL_PROPERTY = "--dshm-composer-restore-label";
176
+ const CONVERSATION_SCROLL_SELECTOR = "[data-conversation-scroll]";
177
+ const COMPOSER_CARD_SELECTOR$1 = "[data-composer-card]";
178
+ const COMPOSER_EDITOR_SELECTOR$1 = `${COMPOSER_CARD_SELECTOR$1} :is([contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""],textarea)`;
179
+ /**
180
+ * Install the reading-mode collapse. Opt-in through the persisted switch, off by
181
+ * default; the composer and its draft stay owned by DSH, this only toggles a
182
+ * presentation attribute on the document element.
183
+ */
184
+ function installComposerReadingMode(options) {
185
+ const doc = options.doc ?? globalThis.document;
186
+ const root = doc.documentElement;
187
+ const tracker = new ComposerCollapseTracker(composerCollapseConfigFrom(readComposerCollapseEnabled()));
188
+ let collapsed = false;
189
+ const view = doc.defaultView;
190
+ const apply = (next) => {
191
+ if (collapsed === next) return;
192
+ collapsed = next;
193
+ root.setAttribute(COLLAPSED_ATTRIBUTE, next ? "true" : "false");
194
+ };
195
+ const expand = () => {
196
+ tracker.expand();
197
+ apply(false);
198
+ };
199
+ root.style.setProperty(RESTORE_LABEL_PROPERTY, JSON.stringify(options.restoreLabel));
200
+ const scroller = () => {
201
+ const found = doc.querySelector(CONVERSATION_SCROLL_SELECTOR);
202
+ return found instanceof (view?.HTMLElement ?? Object) ? found : void 0;
203
+ };
204
+ let lastTop = scroller()?.scrollTop ?? 0;
205
+ let rafPending = false;
206
+ const sample = () => {
207
+ rafPending = false;
208
+ const port = scroller();
209
+ if (port === void 0) return;
210
+ const top = port.scrollTop;
211
+ const delta = top - lastTop;
212
+ lastTop = top;
213
+ if (busy()) {
214
+ expand();
215
+ return;
216
+ }
217
+ apply(tracker.sample(delta) === "collapsed");
218
+ };
219
+ const onScroll = (event) => {
220
+ const target = event.target;
221
+ if (target === null || !target.hasAttribute?.(CONVERSATION_SCROLL_SELECTOR.slice(1, -1))) return;
222
+ if (rafPending) return;
223
+ rafPending = true;
224
+ if (view === null) {
225
+ sample();
226
+ return;
227
+ }
228
+ view.requestAnimationFrame(sample);
229
+ };
230
+ const busy = () => {
231
+ if (options.isRunning()) return true;
232
+ const card = doc.querySelector(COMPOSER_CARD_SELECTOR$1);
233
+ const editor = card?.querySelector(COMPOSER_EDITOR_SELECTOR$1) ?? null;
234
+ if (editor === null) return false;
235
+ const phase = editor.dataset.phase ?? "";
236
+ if (phase !== "" && phase !== "plain") return true;
237
+ const focused = doc.activeElement;
238
+ if (focused !== null && card !== null && card.contains(focused)) return true;
239
+ return (editor.value ?? editor.textContent ?? "").trim() !== "";
240
+ };
241
+ const guard = () => {
242
+ if (busy()) expand();
243
+ };
244
+ const onPointerDown = (event) => {
245
+ if (!collapsed) return;
246
+ const target = event.target;
247
+ if (target === null || !(target instanceof (view?.Element ?? Object))) return;
248
+ if (target.closest(COMPOSER_CARD_SELECTOR$1) === null) return;
249
+ expand();
250
+ };
251
+ const onFocusIn = () => {
252
+ expand();
253
+ };
254
+ let observed = [];
255
+ const observeCards = () => {
256
+ const cards = [...doc.querySelectorAll(COMPOSER_CARD_SELECTOR$1)];
257
+ if (cards.length === observed.length && cards.every((card, index) => card === observed[index])) return;
258
+ observer.disconnect();
259
+ for (const card of cards) observer.observe(card, {
260
+ childList: true,
261
+ subtree: true,
262
+ characterData: true,
263
+ attributes: true,
264
+ attributeFilter: ["data-phase"]
265
+ });
266
+ observed = cards;
267
+ };
268
+ const observer = new ((view?.MutationObserver) ?? MutationObserver)(() => {
269
+ observeCards();
270
+ guard();
271
+ });
272
+ observeCards();
273
+ doc.addEventListener("scroll", onScroll, {
274
+ passive: true,
275
+ capture: true
276
+ });
277
+ doc.addEventListener("pointerdown", onPointerDown, true);
278
+ doc.addEventListener("focusin", onFocusIn, true);
279
+ const stopPreference = subscribeComposerCollapseEnabled((enabled) => {
280
+ tracker.setEnabled(enabled);
281
+ if (!enabled) expand();
282
+ });
283
+ const stopViewport = onViewportResize(onFocusIn);
284
+ return {
285
+ expand,
286
+ dispose() {
287
+ doc.removeEventListener("scroll", onScroll, { capture: true });
288
+ doc.removeEventListener("pointerdown", onPointerDown, true);
289
+ doc.removeEventListener("focusin", onFocusIn, true);
290
+ stopViewport();
291
+ stopPreference();
292
+ observer.disconnect();
293
+ root.style.removeProperty(RESTORE_LABEL_PROPERTY);
294
+ apply(false);
295
+ }
296
+ };
297
+ }
298
+ /**
299
+ * Whether the active session is streaming a reply. A running reply is not a
300
+ * composer phase: `data-phase` returns to `plain` as soon as the submission is
301
+ * accepted, so this flag is the only truthful signal for the streaming window.
302
+ */
303
+ function selectActiveSessionRunning(state) {
304
+ const current = state.current;
305
+ return current !== void 0 && state.byId[current]?.running === true;
306
+ }
307
+ // src/session-prefetch.ts
308
+ const SESSION_PREFETCH_DEFAULTS = {
309
+ enabled: false,
310
+ settleMs: 1500,
311
+ cooldownMs: 6e4,
312
+ timeoutMs: 2e4,
313
+ maxTargetsPerSwitch: 1
314
+ };
315
+ /**
316
+ * Storage key for the opt-in switch.
317
+ *
318
+ * The setting row lives in the client bundle while the tracker lives in the
319
+ * layout bundle (tsdown.config.ts emits them separately and inlines this module
320
+ * into each), so a module-level singleton cannot carry the value across. The
321
+ * layout bundle owns the persisted state instead; the row notifies it through
322
+ * both the storage event (other tabs) and a custom event (same page).
323
+ */
324
+ const SESSION_PREFETCH_STORAGE_KEY = "dsh-mobile.session-prefetch.enabled";
325
+ const SESSION_PREFETCH_EVENT = "dsh-mobile:session-prefetch-change";
326
+ /** Read the persisted opt-in. Defaults to off; unreadable storage keeps it off. */
327
+ function readSessionPrefetchEnabled(storage) {
328
+ try {
329
+ return (storage ?? globalThis.localStorage)?.getItem(SESSION_PREFETCH_STORAGE_KEY) === "true";
330
+ } catch {
331
+ return false;
332
+ }
333
+ }
334
+ /**
335
+ * Subscribe to opt-in changes coming from either bundle. The listener is not
336
+ * called with the initial value; read it once with readSessionPrefetchEnabled().
337
+ */
338
+ function subscribeSessionPrefetchEnabled(listener) {
339
+ const onStorage = (event) => {
340
+ if (event.key !== null && event.key !== "dsh-mobile.session-prefetch.enabled") return;
341
+ listener(readSessionPrefetchEnabled());
342
+ };
343
+ const onCustom = (event) => {
344
+ const detail = event.detail;
345
+ listener(detail?.enabled ?? readSessionPrefetchEnabled());
346
+ };
347
+ globalThis.addEventListener?.("storage", onStorage);
348
+ globalThis.addEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
349
+ return () => {
350
+ globalThis.removeEventListener?.("storage", onStorage);
351
+ globalThis.removeEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
352
+ };
353
+ }
354
+ /** Apply the persisted opt-in onto a base config. */
355
+ function sessionPrefetchConfigFrom(enabled, base = SESSION_PREFETCH_DEFAULTS) {
356
+ return {
357
+ ...base,
358
+ enabled
359
+ };
360
+ }
361
+ /** In-memory per-session cooldown ledger plus MRU switch tracking (pure). */
362
+ var SessionPrefetchTracker = class {
363
+ config;
364
+ clock;
365
+ cooldownUntil = /* @__PURE__ */ new Map();
366
+ previousSession;
367
+ lastSwitchAt = 0;
368
+ constructor(config, clock = { now: () => Date.now() }) {
369
+ this.config = config;
370
+ this.clock = clock;
371
+ }
372
+ /** The effective opt-in this tracker was built with (read-only, for rebuilds). */
373
+ get enabled() {
374
+ return this.config.enabled;
375
+ }
376
+ /** Record that the UI switched to `sessionId`; returns the switch outcome. */
377
+ switchTo(sessionId) {
378
+ if (sessionId === "") return {
379
+ action: "skip",
380
+ reason: "no-previous"
381
+ };
382
+ const previous = this.previousSession;
383
+ this.previousSession = sessionId;
384
+ this.lastSwitchAt = this.clock.now();
385
+ if (previous === void 0) return {
386
+ action: "skip",
387
+ reason: "no-previous"
388
+ };
389
+ if (previous === sessionId) return {
390
+ action: "skip",
391
+ reason: "same-session"
392
+ };
393
+ if (!this.config.enabled) return {
394
+ action: "skip",
395
+ reason: "disabled"
396
+ };
397
+ const cooldownUntil = this.cooldownUntil.get(previous) ?? 0;
398
+ if (this.clock.now() < cooldownUntil) return {
399
+ action: "skip",
400
+ reason: "cooldown"
401
+ };
402
+ this.cooldownUntil.set(previous, this.clock.now() + this.config.cooldownMs);
403
+ return {
404
+ action: "warm",
405
+ target: previous
406
+ };
407
+ }
408
+ /** Whether the switch already passed the settle window (pure predicate). */
409
+ settled(now = this.clock.now()) {
410
+ return now - this.lastSwitchAt >= this.config.settleMs;
411
+ }
412
+ /** Forget state (used when the client unmounts). */
413
+ reset() {
414
+ this.cooldownUntil.clear();
415
+ this.previousSession = void 0;
416
+ this.lastSwitchAt = 0;
417
+ }
418
+ };
419
+ /**
420
+ * Fire a read-only `session/projections` prefetch, silently ignoring failures.
421
+ *
422
+ * The upstream RPC takes a single `request` parameter and the gateway validates
423
+ * wire names exactly (`assertExactArguments`), so the envelope must be
424
+ * `payload.args.request.sessionId` — the same shape every other call site in
425
+ * this repo uses (scripts/smoke-client-module-settings.mjs, smoke-draft-transfer.mjs).
426
+ * A bare `args.sessionId` is rejected with `gateway/arguments-invalid`.
427
+ */
428
+ function prefetchSession(request, sessionId, timeoutMs) {
429
+ const controller = new AbortController();
430
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
431
+ (async () => {
432
+ try {
433
+ if (!(await request("/api/session/projections", {
434
+ method: "POST",
435
+ headers: { "content-type": "application/json" },
436
+ body: JSON.stringify({
437
+ type: "client-request",
438
+ rpcId: prefetchRpcId(),
439
+ method: "session/projections",
440
+ payload: { args: { request: { sessionId } } }
441
+ }),
442
+ signal: controller.signal
443
+ })).ok) return;
444
+ } catch {} finally {
445
+ clearTimeout(timer);
446
+ }
447
+ })();
448
+ return () => {
449
+ controller.abort();
450
+ clearTimeout(timer);
451
+ };
452
+ }
453
+ /** Unique per attempt; the upstream does not document a format, so prefer a real UUID. */
454
+ function prefetchRpcId() {
455
+ const cryptoApi = globalThis.crypto;
456
+ if (cryptoApi?.randomUUID !== void 0) return cryptoApi.randomUUID();
457
+ return `dsh-mobile-prefetch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
458
+ }
40
459
  // src/mobile-layout.ts
41
460
  /** Whether an optional service value supports the public right-Sidebar controls. */
42
461
  function isSidebarRightControl(value) {
@@ -587,6 +1006,17 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
587
1006
  .dshm-main[data-dsh-panel="1"] [data-slot="main"]>:not([data-dsh-adapt="off"]) :is(button,[role="button"],select,input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=hidden]):not([type=color]):not([type=file]):not([type=image])):not([role="switch"],[role="checkbox"],[role="radio"],[role="slider"],button[class*="_cardTitle"][class*="_cardOpen"],[data-dsh-adapt="off"],[data-dsh-adapt="off"] *){min-width:44px!important;min-height:44px!important}
588
1007
  .dshm-main[data-dsh-panel="1"] [data-slot="main"]>:not([data-dsh-adapt="off"]) :is(button,[role="button"],a):not([data-dsh-adapt="off"],[data-dsh-adapt="off"] *){touch-action:manipulation}
589
1008
  }
1009
+ /* Reading mode (issue #194): after scrolling down a long conversation the
1010
+ composer editor collapses so the reading area is unobstructed. The card stays
1011
+ and keeps the upstream Send/Stop row; its generated label is the way back, and
1012
+ the text comes from --dshm-composer-restore-label so nothing is injected into
1013
+ the React-managed card. Opt-in, off by default. */
1014
+ [data-dsh-mobile-composer-collapsed="true"] [data-composer-card] :is([contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""],textarea){display:none!important}
1015
+ [data-dsh-mobile-composer-collapsed="true"] [data-composer-card]{
1016
+ padding-top:6px!important;padding-bottom:6px!important;min-height:0!important;cursor:pointer}
1017
+ [data-dsh-mobile-composer-collapsed="true"] [data-composer-card]::before{
1018
+ content:var(--dshm-composer-restore-label, "");display:flex;align-items:center;
1019
+ justify-content:center;min-height:44px;font-size:13px;opacity:.75;pointer-events:none}
590
1020
  `;
591
1021
  function MobileAppFrame(props) {
592
1022
  const state = (0, react.useSyncExternalStore)(props.controller.subscribe, props.controller.getSnapshot);
@@ -642,6 +1072,59 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
642
1072
  return current !== void 0 && session.byId[current]?.blank === false ? current : void 0;
643
1073
  });
644
1074
  const hasSession = activeSessionId !== void 0;
1075
+ const prefetchEnabled = (0, react.useSyncExternalStore)(subscribeSessionPrefetchEnabled, readSessionPrefetchEnabled, () => false);
1076
+ const prefetchTracker = (0, react.useRef)(void 0);
1077
+ const prefetchTimer = (0, react.useRef)(void 0);
1078
+ const prefetchDispose = (0, react.useRef)(void 0);
1079
+ if (prefetchTracker.current === void 0 || prefetchTracker.current.enabled !== prefetchEnabled) prefetchTracker.current = new SessionPrefetchTracker(sessionPrefetchConfigFrom(prefetchEnabled));
1080
+ (0, react.useEffect)(() => {
1081
+ if (activeSessionId === void 0) return;
1082
+ const decision = prefetchTracker.current.switchTo(activeSessionId);
1083
+ if (decision.action !== "warm") return;
1084
+ const target = decision.target;
1085
+ if (prefetchTimer.current !== void 0) clearTimeout(prefetchTimer.current);
1086
+ prefetchTimer.current = window.setTimeout(() => {
1087
+ prefetchTimer.current = void 0;
1088
+ prefetchDispose.current?.();
1089
+ prefetchDispose.current = prefetchSession(fetch, target, SESSION_PREFETCH_DEFAULTS.timeoutMs);
1090
+ }, SESSION_PREFETCH_DEFAULTS.settleMs);
1091
+ return () => {
1092
+ if (prefetchTimer.current !== void 0) {
1093
+ clearTimeout(prefetchTimer.current);
1094
+ prefetchTimer.current = void 0;
1095
+ }
1096
+ prefetchDispose.current?.();
1097
+ prefetchDispose.current = void 0;
1098
+ };
1099
+ }, [activeSessionId, prefetchEnabled]);
1100
+ const collapseEnabled = (0, react.useSyncExternalStore)(subscribeComposerCollapseEnabled, readComposerCollapseEnabled, () => false);
1101
+ const activeRunning = props.useSessions(selectActiveSessionRunning);
1102
+ const runningRef = (0, react.useRef)(activeRunning);
1103
+ runningRef.current = activeRunning;
1104
+ const active = collapseEnabled && hasSession;
1105
+ const readingMode = (0, react.useRef)(void 0);
1106
+ (0, react.useEffect)(() => {
1107
+ if (!active) return;
1108
+ const installed = installComposerReadingMode({
1109
+ restoreLabel: messages.composerRestore,
1110
+ isRunning: () => runningRef.current
1111
+ });
1112
+ readingMode.current = installed;
1113
+ if (runningRef.current) installed.expand();
1114
+ return () => {
1115
+ readingMode.current = void 0;
1116
+ installed.dispose();
1117
+ };
1118
+ }, [
1119
+ active,
1120
+ activeSessionId,
1121
+ messages.composerRestore
1122
+ ]);
1123
+ (0, react.useEffect)(() => {
1124
+ if (!activeRunning) return;
1125
+ readingMode.current?.expand();
1126
+ document.documentElement.setAttribute("data-dsh-mobile-composer-collapsed", "false");
1127
+ }, [activeRunning]);
645
1128
  (0, react.useEffect)(() => {
646
1129
  const observer = new MutationObserver(() => {
647
1130
  setDocumentLanguage(document.documentElement.lang);
@@ -925,6 +1408,7 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
925
1408
  exports.apply = apply;
926
1409
  exports.closeDetailsFromScrim = closeDetailsFromScrim;
927
1410
  exports.inject = inject;
1411
+ exports.installComposerReadingMode = installComposerReadingMode;
928
1412
  exports.installComposerSendKeyboardPolicy = installComposerSendKeyboardPolicy;
929
1413
  exports.isComposerActionTap = isComposerActionTap;
930
1414
  exports.isComposerOwnedFocus = isComposerOwnedFocus;
@@ -932,10 +1416,12 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
932
1416
  exports.isSessionRowNavigation = isSessionRowNavigation;
933
1417
  exports.isSidebarRightControl = isSidebarRightControl;
934
1418
  exports.isWideViewportLayout = isWideViewportLayout;
1419
+ exports.readComposerCollapseEnabled = readComposerCollapseEnabled;
935
1420
  exports.resolveComposerImePolicy = resolveComposerImePolicy;
936
1421
  exports.resolveDeviceShortSide = resolveDeviceShortSide;
937
1422
  exports.resolveMobileLayoutLanguage = resolveMobileLayoutLanguage;
938
1423
  exports.resolveMobileRightbarLayout = resolveMobileRightbarLayout;
1424
+ exports.selectActiveSessionRunning = selectActiveSessionRunning;
939
1425
  return module.exports;
940
1426
  }
941
1427
  });