dsh-mobile 0.6.17 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +47 -11
  2. package/CONTRIBUTORS.md +12 -6
  3. package/README.en.md +22 -14
  4. package/README.md +21 -14
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY_NOTICES.md +33 -0
  7. package/apps/mobile/README.md +28 -3
  8. package/apps/mobile/README.zh-CN.md +28 -3
  9. package/apps/mobile/release.json +3 -3
  10. package/cordis.patch.yml +5 -9
  11. package/docs/ATTACH_EXISTING_FRPS.en.md +32 -2
  12. package/docs/ATTACH_EXISTING_FRPS.md +2 -2
  13. package/docs/CLIENT_MODULES.en.md +2 -0
  14. package/docs/CLIENT_MODULES.md +2 -0
  15. package/docs/CLOUDFLARE_TUNNEL.en.md +2 -0
  16. package/docs/CLOUDFLARE_TUNNEL.md +2 -0
  17. package/docs/IROH.en.md +44 -0
  18. package/docs/IROH.md +44 -0
  19. package/docs/MOBILE_PLUGIN_ADAPTATION.en.md +196 -0
  20. package/docs/MOBILE_PLUGIN_ADAPTATION.md +196 -0
  21. package/docs/README.md +9 -0
  22. package/docs/SELF_HOSTED_ORIGIN.en.md +26 -1
  23. package/docs/SELF_HOSTED_ORIGIN.md +27 -2
  24. package/docs/SKIN_COMPATIBILITY.en.md +2 -0
  25. package/docs/SKIN_COMPATIBILITY.md +2 -0
  26. package/docs/SLOW_CONNECTIONS.en.md +10 -2
  27. package/docs/SLOW_CONNECTIONS.md +10 -2
  28. package/docs/TRUSTED_NETWORKS.en.md +17 -0
  29. package/docs/TRUSTED_NETWORKS.md +17 -0
  30. package/lib/cli.js.map +1 -1
  31. package/lib/client.js +1081 -70
  32. package/lib/client.js.map +1 -1
  33. package/lib/index.d.mts +271 -2
  34. package/lib/index.mjs +1908 -245
  35. package/lib/index.mjs.map +1 -1
  36. package/lib/mobile-layout.js +14 -6
  37. package/lib/mobile-layout.js.map +1 -1
  38. package/native/iroh-host/LICENSE.dsh-mobile-iroh +21 -0
  39. package/native/iroh-host/LICENSE.dsh-tether +21 -0
  40. package/native/iroh-host/NOTICE.dsh-mobile-iroh +13 -0
  41. package/native/iroh-host/README.md +66 -0
  42. package/package.json +18 -5
  43. package/packages/question-fixes/package.json +1 -1
@@ -404,8 +404,9 @@ window.__ModuleLoader__.load({
404
404
  };
405
405
  const MOBILE_LAYOUT_STYLES = `
406
406
  html,body,#root{width:100%;height:100%;overflow:hidden}
407
- /* The definite root height bounds the transcript on engines without dvh. */
408
- .dshm-shell{position:relative;display:grid;grid-template-rows:minmax(0,1fr);width:100%;height:100%;height:100dvh;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#fff)}
407
+ /* Definite height bounds the transcript on engines without dvh. Isolation keeps
408
+ shell descendants below positive-z-index portals attached to document.body. */
409
+ .dshm-shell{position:relative;isolation:isolate;display:grid;grid-template-rows:minmax(0,1fr);width:100%;height:100%;height:100dvh;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#fff)}
409
410
  /* The mobile root owns its Grid geometry. Layered important rules keep broad
410
411
  unlayered skin rules from collapsing virtualized views to the header height. */
411
412
  @layer dsh-mobile-layout{
@@ -419,8 +420,13 @@ html,body,#root{width:100%;height:100%;overflow:hidden}
419
420
  /* Draw above the native absolute panel without consuming its width or
420
421
  intercepting controls; only docked mode needs this column separator. */
421
422
  .dshm-shell[data-rightbar-docked=true] .dshm-details::after{content:"";position:absolute;inset:0 auto 0 0;width:.5px;background:var(--dsw-alias-border-l4);z-index:11;pointer-events:none}
423
+ /* The native panel paints itself at 100vw in fullscreen mode. Inside the
424
+ docked overlay (94vw/460px, overflow:hidden) that layout was clipped to the
425
+ right edge of the track, so the header slid out of view and the body showed
426
+ only clipped line tails. A fullscreen panel owns the whole viewport. */
427
+ .dshm-shell[data-rightbar-fullscreen=true] .dshm-details{inset:0;width:100vw;max-width:100vw;box-shadow:none}
422
428
  .dshm-main>*,.dshm-main>*>*{min-width:0}
423
- .dshm-drawer{position:fixed;z-index:70;inset:0 auto 0 0;box-sizing:border-box;width:56px;max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#f8fafc);box-shadow:none;will-change:width;transition:width 240ms cubic-bezier(.22,1,.36,1),box-shadow 240ms ease}
429
+ .dshm-drawer{position:fixed;z-index:70;inset:0 auto 0 0;box-sizing:border-box;width:56px;max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-layer-1,#f8fafc),var(--dsw-alias-bg-layer-1,#f8fafc));box-shadow:none;will-change:width;transition:width 240ms cubic-bezier(.22,1,.36,1),box-shadow 240ms ease}
424
430
  .dshm-drawer[data-open=true]{width:min(88vw,340px);box-shadow:18px 0 46px rgb(15 23 42 / 18%)}
425
431
  .dshm-drawer[data-open=false]{pointer-events:auto;visibility:visible}
426
432
  .dshm-drawer>*{width:100%!important;height:100%!important;transform:translateX(-6px);opacity:.94;transition:transform 220ms cubic-bezier(.22,1,.36,1),opacity 160ms ease-out}
@@ -428,7 +434,7 @@ html,body,#root{width:100%;height:100%;overflow:hidden}
428
434
  .dshm-drawer[data-open=false]>*{width:56px!important}
429
435
  /* display:none also suppresses fixed descendants installed by the phone adapter. */
430
436
  .dshm-drawer[data-right-modal=true]{display:none!important}
431
- .dshm-details{position:fixed;z-index:80;inset:0 0 0 auto;box-sizing:border-box;width:min(94vw,460px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:-18px 0 46px rgb(15 23 42 / 18%);transform:translateX(104%);transition:transform 190ms cubic-bezier(.22,1,.36,1)}
437
+ .dshm-details{position:fixed;z-index:80;inset:0 0 0 auto;box-sizing:border-box;width:min(94vw,460px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-layer-1,#fff),var(--dsw-alias-bg-layer-1,#fff));box-shadow:-18px 0 46px rgb(15 23 42 / 18%);transform:translateX(104%);transition:transform 190ms cubic-bezier(.22,1,.36,1)}
432
438
  .dshm-details[data-open=true]{transform:translateX(0)}
433
439
  .dshm-details[data-open=false]{pointer-events:none;visibility:hidden;transition:transform 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}
434
440
  @media(max-width:899px){.dshm-details{width:100%;max-width:100%;box-shadow:none}.dshm-details [data-sidebar-right-toggle],.dshm-details [data-sidebar-right-mode]{min-width:48px;min-height:48px}}
@@ -436,7 +442,7 @@ html,body,#root{width:100%;height:100%;overflow:hidden}
436
442
  .dshm-scrim[data-open=true]{opacity:1;pointer-events:auto}
437
443
  .dshm-overlay{position:fixed;z-index:90;inset:0;pointer-events:none}.dshm-overlay>*{pointer-events:auto}
438
444
  /* Reserve navigation space instead of covering a panel's own save controls. */
439
- .dshm-panelNav{display:flex;align-items:center;flex-shrink:0;min-height:56px;box-sizing:border-box;padding:max(4px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 4px max(68px,env(safe-area-inset-left));border-bottom:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-base,#fff)}
445
+ .dshm-panelNav{display:flex;align-items:center;flex-shrink:0;min-height:56px;box-sizing:border-box;padding:max(4px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 4px max(68px,env(safe-area-inset-left));border-bottom:.5px solid var(--dsw-alias-border-l4);background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff))}
440
446
  .dshm-panelBack{display:inline-flex;align-items:center;gap:8px;min-width:48px;min-height:48px;padding:8px 12px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,#171a21);font:inherit;font-size:14px;line-height:1.4;text-align:left;cursor:pointer}
441
447
  .dshm-panelBack:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
442
448
  .dshm-panelBack:active{background:var(--dsw-alias-interactive-bg-active,#e9ecef)}
@@ -523,7 +529,8 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
523
529
  const wideViewport = isWideViewportLayout(viewportWidth);
524
530
  const rightbar = resolveMobileRightbarLayout(viewportWidth, state.sidebarOpen, state.rightbarTrack, state.rightbarFullscreen);
525
531
  const rightbarDocked = state.detailsOpen && rightbar.docked;
526
- const rightbarFullWidth = state.detailsOpen && !rightbarDocked && !wideViewport;
532
+ const rightbarFullscreen = state.detailsOpen && state.rightbarFullscreen;
533
+ const rightbarFullWidth = state.detailsOpen && !rightbarDocked && (!wideViewport || rightbarFullscreen);
527
534
  const [documentLanguage, setDocumentLanguage] = (0, react.useState)(document.documentElement.lang);
528
535
  const language = resolveMobileLayoutLanguage(documentLanguage, navigator.languages.length > 0 ? navigator.languages : [navigator.language]);
529
536
  const messages = MOBILE_LAYOUT_MESSAGES[language];
@@ -638,6 +645,7 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
638
645
  className: "dshm-shell",
639
646
  lang: language,
640
647
  "data-rightbar-docked": rightbarDocked,
648
+ "data-rightbar-fullscreen": rightbarFullscreen,
641
649
  style: { "--dshm-rightbar-width": `${rightbar.width}px` }
642
650
  }, (0, react.createElement)("main", {
643
651
  className: "dshm-main",
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-layout.js","names":["useSyncExternalStore","useRef","useState","createElement"],"sources":["../src/mobile-layout-messages.ts","../src/mobile-layout.ts"],"sourcesContent":["export type MobileLayoutLanguage = 'it' | 'en' | 'zh'\n\nexport const MOBILE_LAYOUT_MESSAGES = Object.freeze({\n it: Object.freeze({ closePanels: 'Chiudi pannelli', workspaceNavigation: 'Navigazione area di lavoro e sessioni', backToConversation: 'Torna alla conversazione' }),\n en: Object.freeze({ closePanels: 'Close panels', workspaceNavigation: 'Workspace and session navigation', backToConversation: 'Back to conversation' }),\n zh: Object.freeze({ closePanels: '关闭浮层', workspaceNavigation: '工作区与会话导航', backToConversation: '返回会话' }),\n})\n","import { createElement, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { ReactNode } from 'react'\nimport { MOBILE_LAYOUT_MESSAGES, type MobileLayoutLanguage } from './mobile-layout-messages.js'\n\nexport { MOBILE_LAYOUT_MESSAGES } from './mobile-layout-messages.js'\nexport type { MobileLayoutLanguage } from './mobile-layout-messages.js'\n\ninterface SessionState {\n readonly current?: string\n readonly byId: Readonly<Record<string, { readonly blank?: boolean; readonly title?: string } | undefined>>\n}\n\ninterface MobileRootProps {\n readonly renderSlot: (\n name: string,\n owner: Record<string, unknown>,\n options?: { readonly entryKey?: string | null; readonly fallback?: ReactNode },\n ) => ReactNode\n readonly useSessions: <T>(selector: (state: SessionState) => T) => T\n}\n\n/** Root contribution carrying one observable hook into the standard kit. */\ninterface RootHookContribution {\n readonly hooks: Readonly<Record<string, {\n readonly getSnapshot: () => unknown\n readonly subscribe: (listener: () => void) => () => void\n }>>\n}\n\ninterface MobileClientContext {\n readonly effect: (effect: () => void | (() => void), label?: string) => void\n readonly get: (name: string) => unknown\n readonly on: (event: string, listener: (value: ThemeSnapshot) => void) => () => void\n readonly reflect: { provide: (name: string, value: unknown) => () => void | Promise<void> }\n readonly slots: {\n register: <Props>(options: Record<string, unknown>, component: (props: Props) => ReactNode) => () => void\n entries: (name: string) => readonly { readonly options: { readonly key?: string } }[]\n subscribe: (name: string, listener: () => void) => () => void\n /**\n * Publish a root standard hook. The official layout owns `panelInfo`, so\n * replacing that module means replacing the owner too.\n */\n provideRoot?: (contribution: RootHookContribution) => () => void\n }\n readonly theme: { getTheme: () => ThemeSnapshot }\n}\n\ninterface ThemeSnapshot {\n readonly active: {\n readonly colorScheme: 'dark' | 'light'\n readonly tokens: Readonly<Record<string, string>>\n }\n}\n\ninterface LayoutSnapshot {\n readonly sidebarOpen: boolean\n readonly detailsOpen: boolean\n readonly rightbarTrack: boolean\n readonly rightbarFullscreen: boolean\n readonly panelInfo: PanelInfoSnapshot\n}\n\n/**\n * Panel selection as the standard `usePanelInfo` hook reports it: a null\n * `activePanelId` means the conversation owns the center column.\n */\ninterface PanelInfoSnapshot {\n readonly activePanelId: string | null\n}\n\n/** Optional DSH 0.1.5 right-Sidebar controls used by a Mobile close gesture. */\nexport interface SidebarRightControl {\n isExpanded(): boolean\n toggleExpanded(): void\n}\n\n/** Whether an optional service value supports the public right-Sidebar controls. */\nexport function isSidebarRightControl(value: unknown): value is SidebarRightControl {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as Partial<SidebarRightControl>\n return typeof candidate.isExpanded === 'function' && typeof candidate.toggleExpanded === 'function'\n}\n\n/**\n * Viewport width at which the dedicated layout treats the sidebar as a\n * persistent desktop panel instead of an overlay drawer. Narrow screens\n * keep the overlay behavior byte-for-byte.\n */\nexport const WIDE_LAYOUT_MIN_WIDTH_PX = 900\nconst RIGHTBAR_MIN_WIDTH_PX = 300\nconst RIGHTBAR_MAX_WIDTH_PX = 460\nconst RIGHTBAR_CENTER_MIN_WIDTH_PX = 400\nconst RIGHTBAR_DEFAULT_RATIO = 0.45\nconst SIDEBAR_EXPANDED_WIDTH_PX = 340\nconst SIDEBAR_COLLAPSED_WIDTH_PX = 56\n\nexport function isWideViewportLayout(viewportWidth: number): boolean {\n return viewportWidth >= WIDE_LAYOUT_MIN_WIDTH_PX\n}\n\n/** Resolved right-panel presentation for the dedicated responsive layout. */\nexport interface MobileRightbarLayout {\n readonly docked: boolean\n readonly width: number\n}\n\n/** Preserve a readable conversation width before turning a requested right track into a docked column. */\nexport function resolveMobileRightbarLayout(\n viewportWidth: number,\n sidebarOpen: boolean,\n track: boolean,\n fullscreen: boolean,\n): MobileRightbarLayout {\n const overlayWidth = isWideViewportLayout(viewportWidth)\n ? Math.min(viewportWidth * 0.94, RIGHTBAR_MAX_WIDTH_PX)\n : viewportWidth\n if (!track || fullscreen || (!isWideViewportLayout(viewportWidth) && sidebarOpen)) {\n return Object.freeze({ docked: false, width: overlayWidth })\n }\n const sidebarWidth = isWideViewportLayout(viewportWidth) && sidebarOpen\n ? SIDEBAR_EXPANDED_WIDTH_PX\n : SIDEBAR_COLLAPSED_WIDTH_PX\n const available = Math.floor(viewportWidth - sidebarWidth - RIGHTBAR_CENTER_MIN_WIDTH_PX)\n if (available < RIGHTBAR_MIN_WIDTH_PX) return Object.freeze({ docked: false, width: overlayWidth })\n const preferred = Math.min(\n RIGHTBAR_MAX_WIDTH_PX,\n Math.max(RIGHTBAR_MIN_WIDTH_PX, Math.round(viewportWidth * RIGHTBAR_DEFAULT_RATIO)),\n )\n return Object.freeze({ docked: true, width: Math.min(preferred, available) })\n}\n\n/** Whether an overlay drawer needs the modal scrim without dimming a docked desktop panel. */\nexport function isMobileScrimOpen(\n sidebarOpen: boolean,\n detailsModalOpen: boolean,\n wideViewport: boolean,\n): boolean {\n return (!wideViewport && sidebarOpen) || detailsModalOpen\n}\n\n/**\n * Query matching devices whose primary pointer cannot hover: the phones and\n * tablets where focusing an editor raises a soft keyboard the user did not ask\n * for. A desktop keeps stock behaviour because it has a real keyboard already.\n */\nexport const TOUCH_PRIMARY_QUERY = '(hover: none), (pointer: coarse)'\n\n/** The stock composer wrapper, which owns everything the phone adapter edits. */\nconst COMPOSER_CARD_SELECTOR = '[data-composer-card]'\n\n/** The composer's editing host, whichever contenteditable flavour it boots with. */\nconst COMPOSER_EDITOR_SELECTOR = `${COMPOSER_CARD_SELECTOR} :is([contenteditable=\"true\"],[contenteditable=\"plaintext-only\"],[contenteditable=\"\"])`\n\n/** The composer's Add trigger, whose menu must open without summoning the IME. */\nconst COMPOSER_COMMAND_TRIGGER_SELECTOR = `${COMPOSER_CARD_SELECTOR} button[aria-haspopup=\"listbox\"]`\n\n/**\n * Whether a focus target belongs to the composer, which owns its own keyboard\n * decision and must never be blurred by a suppression window.\n * @param target - the focus target, already narrowed to an element.\n * @returns whether the target sits inside the composer card.\n */\nexport function isComposerOwnedFocus(target: Element | null): boolean {\n return target !== null && target.closest(COMPOSER_CARD_SELECTOR) !== null\n}\n\n/**\n * Whether a click opens a different Session, rather than its menu or current row.\n * @param target - the click target in the workspace sidebar.\n * @returns whether the Session selection can change.\n */\nexport function isSessionRowNavigation(target: Element | null): boolean {\n if (target === null) return false\n const row = target.closest('[role=\"treeitem\"][aria-selected]')\n if (row === null || row.getAttribute('aria-selected') === 'true') return false\n const action = target.closest('button,[role=\"button\"]')\n return action === null || action === row\n}\n\n/**\n * Decide how one pointer event changes the composer's soft keyboard.\n *\n * The Add button's own mousedown calls `keepDraftFocus`, which focuses the draft\n * on purpose so the caret survives the menu; on a phone that focus is what\n * raises the keyboard. `inputmode=\"none\"` withholds the IME without moving focus\n * or the caret, so the menu, the caret, and the draft all keep stock behaviour.\n * @param target - the pointer target, already narrowed to an element.\n * @param touchPrimary - whether the device's primary pointer cannot hover.\n * @returns `withhold` for the Add trigger, `restore` for any other tap, and\n * `ignore` where no soft keyboard exists to withhold.\n */\nexport function resolveComposerImePolicy(\n target: Element | null,\n touchPrimary: boolean,\n): 'ignore' | 'restore' | 'withhold' {\n if (!touchPrimary || target === null) return 'ignore'\n return target.closest(COMPOSER_COMMAND_TRIGGER_SELECTOR) === null ? 'restore' : 'withhold'\n}\n\n/**\n * Whether a tap landed on the stock Send, Queue, or Steer primary button.\n * Stop uses a square SVG; Add, attachment, and plugin buttons are separate actions.\n * @param target - the click target in the composer card.\n * @returns whether the enabled primary action carries the send arrow.\n */\nexport function isComposerActionTap(target: Element | null): boolean {\n if (target === null) return false\n const action = target.closest('button[class*=\"_primary\"]')\n return action !== null && action.closest(COMPOSER_CARD_SELECTOR) !== null\n && !action.hasAttribute('disabled') && action.getAttribute('aria-disabled') !== 'true'\n && action.querySelector('svg path') !== null\n}\n\n/** Text, reference chips, and the stock attachment rail all contribute to a draft. */\nfunction composerHasDraft(editor: HTMLElement, card: Element): boolean {\n return (editor.textContent ?? '').replaceAll('\\u200b', '').trim() !== ''\n || editor.querySelector('[contenteditable=\"false\"],[data-lexical-decorator],img') !== null\n || card.querySelector('[data-slot=\"conversation.input.attachments\"] [role=\"group\"] > *') !== null\n}\n\n/**\n * Collapse an on-screen keyboard after a trusted touch Send consumes the focused\n * draft. Consumption may be asynchronous; input, navigation, composition, or\n * another gesture cancels the observation. DSH owns optimistic sends and any\n * later failure restoration; this listener never submits or modifies the draft.\n * @param onConsumed - called immediately before blurring to suppress DSH autofocus.\n * @returns disposer for pointer, composition, focus, and draft observations.\n */\nexport function installComposerSendKeyboardPolicy(onConsumed: () => void): () => void {\n let touchButton: Element | null = null\n let cancelPending = (): void => {}\n const composing = new WeakSet<Element>()\n let compositionEndedAt = -Infinity\n const elementTarget = (target: EventTarget | null): Element | null => target instanceof Element ? target : null\n const onPointerDown = (event: PointerEvent): void => {\n cancelPending()\n const target = elementTarget(event.target)\n touchButton = event.isTrusted && (event.pointerType === 'touch' || event.pointerType === 'pen')\n && isComposerActionTap(target) ? target?.closest('button') ?? null : null\n }\n const onClick = (event: MouseEvent): void => {\n const action = touchButton\n touchButton = null\n const target = elementTarget(event.target)\n const active = document.activeElement\n const nativeState = window.__DSH_MOBILE_KEYBOARD_STATE__\n if (!event.isTrusted || event.detail === 0 || action === null || target?.closest('button') !== action\n || !isComposerActionTap(target) || !window.matchMedia(TOUCH_PRIMARY_QUERY).matches\n || (window.__DSH_MOBILE_NATIVE__ !== undefined\n && (nativeState?.noHardwareKeyboard !== true || nativeState.imeVisible !== true))\n || !(active instanceof HTMLElement) || !active.matches(COMPOSER_EDITOR_SELECTOR)\n || composing.has(active) || performance.now() - compositionEndedAt < 10) return\n const editor = active\n const composerCard = editor.closest(COMPOSER_CARD_SELECTOR)\n if (composerCard === null || action.closest(COMPOSER_CARD_SELECTOR) !== composerCard || !composerHasDraft(editor, composerCard)) return\n const card = composerCard\n const sessionOwner = card.closest('[data-conversation-session]')\n const sessionId = sessionOwner?.getAttribute('data-conversation-session')\n const currentEditor = (): boolean => editor.isConnected && editor.closest(COMPOSER_CARD_SELECTOR) === card\n && card.closest('[data-conversation-session]') === sessionOwner\n && sessionOwner?.getAttribute('data-conversation-session') === sessionId\n && document.activeElement === editor\n cancelPending()\n let frame = 0\n const observer = new MutationObserver(check)\n const expiry = window.setTimeout(() => { cancelPending() }, 30_000)\n cancelPending = (): void => {\n observer.disconnect()\n window.clearTimeout(expiry)\n window.cancelAnimationFrame(frame)\n cancelPending = () => {}\n }\n function check(): void {\n if (!currentEditor()) {\n cancelPending()\n return\n }\n if (composerHasDraft(editor, card) || ['adjudicating', 'submitting'].includes(editor.dataset.phase ?? '')) return\n if (frame !== 0) return\n frame = window.requestAnimationFrame(() => {\n frame = window.requestAnimationFrame(() => {\n frame = 0\n if (!currentEditor() || composerHasDraft(editor, card)\n || ['adjudicating', 'submitting'].includes(editor.dataset.phase ?? '')) return\n cancelPending()\n onConsumed()\n editor.blur()\n })\n })\n }\n observer.observe(card, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['data-phase', 'contenteditable'] })\n // The capture listener runs before React's onClick; the first frame reads its result.\n frame = window.requestAnimationFrame(() => { frame = 0; check() })\n }\n const onCompositionStart = (event: CompositionEvent): void => {\n const target = elementTarget(event.target)\n if (target !== null) composing.add(target)\n cancelPending()\n }\n const onCompositionEnd = (event: CompositionEvent): void => {\n const target = elementTarget(event.target)\n if (target !== null) composing.delete(target)\n compositionEndedAt = performance.now()\n }\n const onTyping = (): void => { touchButton = null; cancelPending() }\n const onFocus = (): void => { cancelPending() }\n document.addEventListener('pointerdown', onPointerDown, true)\n document.addEventListener('click', onClick, true)\n document.addEventListener('compositionstart', onCompositionStart, true)\n document.addEventListener('compositionend', onCompositionEnd, true)\n document.addEventListener('keydown', onTyping, true)\n document.addEventListener('input', onTyping, true)\n document.addEventListener('focusin', onFocus, true)\n return () => {\n cancelPending()\n document.removeEventListener('pointerdown', onPointerDown, true)\n document.removeEventListener('click', onClick, true)\n document.removeEventListener('compositionstart', onCompositionStart, true)\n document.removeEventListener('compositionend', onCompositionEnd, true)\n document.removeEventListener('keydown', onTyping, true)\n document.removeEventListener('input', onTyping, true)\n document.removeEventListener('focusin', onFocus, true)\n }\n}\n\n/**\n * Close the details drawer from its scrim, collapsing the DSH 0.1.5 right\n * Sidebar first when that optional service owns the visible content.\n */\nexport function closeDetailsFromScrim(sidebarRight: unknown, closeDetails: () => void): void {\n if (isSidebarRightControl(sidebarRight) && sidebarRight.isExpanded()) {\n sidebarRight.toggleExpanded()\n }\n closeDetails()\n}\n\n/** Reads the live viewport; unknown environments (SSR, tests) stay narrow. */\nfunction viewportIsWide(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false\n return window.matchMedia(`(min-width: ${WIDE_LAYOUT_MIN_WIDTH_PX}px)`).matches\n}\n\n/** Resolve the supported language used by the dedicated mobile layout. */\nexport function resolveMobileLayoutLanguage(\n documentLanguage: string,\n browserLanguages: readonly string[],\n): MobileLayoutLanguage {\n return [documentLanguage, ...browserLanguages]\n .map(value => value.trim().toLowerCase().split(/[-_]/u)[0])\n .find((value): value is MobileLayoutLanguage => value === 'it' || value === 'en' || value === 'zh') ?? 'en'\n}\n\nclass MobileLayoutController {\n // Wide viewports start with the persistent sidebar open; applying the\n // layout again (reconnect, refocus) reuses the module singleton below,\n // so an explicit user collapse is never reset.\n private snapshot: LayoutSnapshot = Object.freeze({\n sidebarOpen: viewportIsWide(),\n detailsOpen: false,\n rightbarTrack: false,\n rightbarFullscreen: false,\n panelInfo: Object.freeze({ activePanelId: null }),\n })\n private readonly listeners = new Set<() => void>()\n private hasMainPanel: ((id: string) => boolean) | undefined\n // Pending workspace/session navigation, cancelled whenever a newer one\n // starts. Mirrors the official LayoutController's navigation controller.\n private navigation = new AbortController()\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n readonly getSnapshot = (): LayoutSnapshot => this.snapshot\n\n /**\n * The panel store on the layout service itself (`ctx.layout.panelInfo`),\n * mirroring the official LayoutController of DSH 0.1.7-rc.2+. The root hook\n * wired in `apply` feeds `usePanelInfo` rendering; rc.2 client plugins read\n * this service member directly. Without it `dsh-client-ui-plugin-manager`\n * fails activation on the dedicated mobile frontend (\"web boot: 1 entry did\n * not activate\") and `dsh-client-ui-open-in-app` throws the first time its\n * shortcut target runs.\n */\n readonly panelInfo = {\n getSnapshot: (): PanelInfoSnapshot => this.getSnapshot().panelInfo,\n subscribe: (listener: () => void): (() => void) => this.subscribe(listener),\n }\n\n toggleSidebar(): void {\n this.update({ sidebarOpen: !this.snapshot.sidebarOpen })\n }\n\n openDetails(): void {\n this.update({ detailsOpen: true })\n }\n\n closeDetails(): void {\n this.update({ detailsOpen: false, rightbarTrack: false, rightbarFullscreen: false })\n }\n\n closeSidebar(): void {\n this.update({ sidebarOpen: false })\n }\n\n /**\n * Invalidate the previous navigation and hand back a fresh signal. The\n * workspace controller wraps this around \"open a workspace\" and \"fork a\n * session\" (dsh-client-ui-workspace) — the path behind the sidebar's\n * new-session button. Without it startSession throws inside a\n * `.catch(reason => console.warn('new session failed:', reason))`, so the\n * button silently did nothing.\n */\n beginNavigation(): AbortSignal {\n this.navigation.abort()\n this.navigation = new AbortController()\n return this.navigation.signal\n }\n\n /** Invalidate pending navigations when this layout unloads. */\n dispose(): void {\n this.navigation.abort()\n }\n\n /** Bind panel validation to the live slot registry owned by this application. */\n bindMainPanelRegistry(hasMainPanel: (id: string) => boolean): () => void {\n this.hasMainPanel = hasMainPanel\n return () => {\n if (this.hasMainPanel === hasMainPanel) this.hasMainPanel = undefined\n }\n }\n\n /** Preserve the native seat's distinction between a docked track and fullscreen. */\n openRightbar(track = true, fullscreen = false): void {\n this.update({ detailsOpen: true, rightbarTrack: track, rightbarFullscreen: fullscreen })\n }\n\n /** Hide the right panel. */\n closeRightbar(): void {\n this.closeDetails()\n }\n\n /**\n * Select the main panel rendered in the center column; null means the\n * conversation. Mirrors the official LayoutController so the `panelInfo`\n * root hook stays truthful. Selecting a panel while the narrow overlay\n * drawer covers the center column dismisses that drawer — the user asked to\n * see the panel, not the navigation over it — while a docked wide sidebar\n * stays open.\n */\n selectPanel(panelId: string | null): void {\n if (panelId !== null && this.hasMainPanel?.(panelId) !== true) {\n throw new Error(`layout.selectPanel: main panel \"${panelId}\" is not registered`)\n }\n this.navigation.abort()\n const dismissDrawer = this.snapshot.sidebarOpen && !viewportIsWide()\n const samePanel = this.snapshot.panelInfo.activePanelId === panelId\n if (!dismissDrawer && samePanel) return\n this.update({\n ...(dismissDrawer ? { sidebarOpen: false } : {}),\n ...(samePanel ? {} : { panelInfo: Object.freeze({ activePanelId: panelId }) }),\n })\n }\n\n /** Drop a selected panel once no main-slot entry declares it. */\n retainMainPanels(panelIds: readonly string[]): void {\n const active = this.snapshot.panelInfo.activePanelId\n if (active !== null && !panelIds.includes(active)) {\n this.update({ panelInfo: Object.freeze({ activePanelId: null }) })\n }\n }\n\n private update(next: Partial<LayoutSnapshot>): void {\n const snapshot = Object.freeze({ ...this.snapshot, ...next })\n if (\n snapshot.sidebarOpen === this.snapshot.sidebarOpen\n && snapshot.detailsOpen === this.snapshot.detailsOpen\n && snapshot.rightbarTrack === this.snapshot.rightbarTrack\n && snapshot.rightbarFullscreen === this.snapshot.rightbarFullscreen\n && snapshot.panelInfo === this.snapshot.panelInfo\n ) return\n this.snapshot = snapshot\n for (const listener of this.listeners) listener()\n }\n}\n\nclass ThemePresenter {\n private appliedTokens: string[] = []\n private readonly meta = document.createElement('meta')\n\n constructor() {\n this.meta.name = 'theme-color'\n }\n\n apply(snapshot: ThemeSnapshot): void {\n const scheme = snapshot.active.colorScheme\n document.documentElement.style.colorScheme = scheme\n document.body.toggleAttribute('data-ds-dark-theme', scheme === 'dark')\n for (const name of this.appliedTokens) document.body.style.removeProperty(name)\n this.appliedTokens = []\n for (const [name, value] of Object.entries(snapshot.active.tokens)) {\n document.body.style.setProperty(name, value)\n this.appliedTokens.push(name)\n }\n this.meta.content = getComputedStyle(document.body).backgroundColor\n if (!this.meta.isConnected) document.head.append(this.meta)\n }\n\n dispose(): void {\n document.documentElement.style.removeProperty('color-scheme')\n document.body.removeAttribute('data-ds-dark-theme')\n for (const name of this.appliedTokens) document.body.style.removeProperty(name)\n this.meta.remove()\n }\n}\n\nexport const MOBILE_LAYOUT_STYLES = `\nhtml,body,#root{width:100%;height:100%;overflow:hidden}\n/* The definite root height bounds the transcript on engines without dvh. */\n.dshm-shell{position:relative;display:grid;grid-template-rows:minmax(0,1fr);width:100%;height:100%;height:100dvh;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#fff)}\n/* The mobile root owns its Grid geometry. Layered important rules keep broad\n unlayered skin rules from collapsing virtualized views to the header height. */\n@layer dsh-mobile-layout{\n.dshm-shell{display:grid!important;grid-template-rows:minmax(0,1fr)!important;grid-template-columns:minmax(0,1fr)!important}\n@media(min-width:900px){.dshm-shell{grid-template-columns:auto minmax(0,1fr)!important}}\n}\n.dshm-main{grid-area:1/1;position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;margin-right:0;overflow:hidden;transition:margin-right var(--ds-transition-duration-slow,190ms) var(--ds-ease-in-out,ease)}\n.dshm-mainContent{position:relative;flex:1;min-width:0;min-height:0;overflow:hidden}\n.dshm-shell[data-rightbar-docked=true] .dshm-main{margin-right:var(--dshm-rightbar-width)}\n.dshm-shell[data-rightbar-docked=true] .dshm-details{position:absolute;width:var(--dshm-rightbar-width);box-shadow:none;padding-top:0}\n/* Draw above the native absolute panel without consuming its width or\n intercepting controls; only docked mode needs this column separator. */\n.dshm-shell[data-rightbar-docked=true] .dshm-details::after{content:\"\";position:absolute;inset:0 auto 0 0;width:.5px;background:var(--dsw-alias-border-l4);z-index:11;pointer-events:none}\n.dshm-main>*,.dshm-main>*>*{min-width:0}\n.dshm-drawer{position:fixed;z-index:70;inset:0 auto 0 0;box-sizing:border-box;width:56px;max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#f8fafc);box-shadow:none;will-change:width;transition:width 240ms cubic-bezier(.22,1,.36,1),box-shadow 240ms ease}\n.dshm-drawer[data-open=true]{width:min(88vw,340px);box-shadow:18px 0 46px rgb(15 23 42 / 18%)}\n.dshm-drawer[data-open=false]{pointer-events:auto;visibility:visible}\n.dshm-drawer>*{width:100%!important;height:100%!important;transform:translateX(-6px);opacity:.94;transition:transform 220ms cubic-bezier(.22,1,.36,1),opacity 160ms ease-out}\n.dshm-drawer[data-open=true]>*{transform:translateX(0);opacity:1}\n.dshm-drawer[data-open=false]>*{width:56px!important}\n/* display:none also suppresses fixed descendants installed by the phone adapter. */\n.dshm-drawer[data-right-modal=true]{display:none!important}\n.dshm-details{position:fixed;z-index:80;inset:0 0 0 auto;box-sizing:border-box;width:min(94vw,460px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:-18px 0 46px rgb(15 23 42 / 18%);transform:translateX(104%);transition:transform 190ms cubic-bezier(.22,1,.36,1)}\n.dshm-details[data-open=true]{transform:translateX(0)}\n.dshm-details[data-open=false]{pointer-events:none;visibility:hidden;transition:transform 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}\n@media(max-width:899px){.dshm-details{width:100%;max-width:100%;box-shadow:none}.dshm-details [data-sidebar-right-toggle],.dshm-details [data-sidebar-right-mode]{min-width:48px;min-height:48px}}\n.dshm-scrim{position:fixed;z-index:65;inset:0;border:0;background:rgb(15 23 42 / 40%);opacity:0;pointer-events:none;transition:opacity 180ms ease-out}\n.dshm-scrim[data-open=true]{opacity:1;pointer-events:auto}\n.dshm-overlay{position:fixed;z-index:90;inset:0;pointer-events:none}.dshm-overlay>*{pointer-events:auto}\n/* Reserve navigation space instead of covering a panel's own save controls. */\n.dshm-panelNav{display:flex;align-items:center;flex-shrink:0;min-height:56px;box-sizing:border-box;padding:max(4px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 4px max(68px,env(safe-area-inset-left));border-bottom:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-base,#fff)}\n.dshm-panelBack{display:inline-flex;align-items:center;gap:8px;min-width:48px;min-height:48px;padding:8px 12px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,#171a21);font:inherit;font-size:14px;line-height:1.4;text-align:left;cursor:pointer}\n.dshm-panelBack:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}\n.dshm-panelBack:active{background:var(--dsw-alias-interactive-bg-active,#e9ecef)}\n.dshm-panelBack:disabled{opacity:.5;cursor:default}\n.dshm-panelBack:focus-visible{outline:2px solid var(--dsw-alias-label-primary,#171a21);outline-offset:2px}\n.dshm-panelBack svg{width:20px;height:20px;flex-shrink:0}\n.dshm-shell header{min-width:0;padding-left:52px}\n/* Keep focused editables above Safari's 16px zoom threshold while retaining\n the DSH content-font preference and larger inherited text. Stock menus\n portal into document.body, so the floor scopes from the body itself. */\nbody:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable=plaintext-only],[contenteditable=\"\"]){font-size:max(16px,1em,var(--dsh-content-font-size,1em))!important}\n.dshm-shell table{display:block;max-width:100%;overflow-x:auto}\n.dshm-shell pre{max-width:100%;overflow-x:auto}\n.dshm-shell img,.dshm-shell video,.dshm-shell canvas,.dshm-shell svg{max-width:100%}\n.dshm-shell [data-disclosure-row]{min-width:0;max-width:100%}\n.dshm-shell [data-disclosure-row]>*{min-width:0;overflow-wrap:anywhere}\n.dshm-shell [data-context-fields]>*{min-width:0}\n.dshm-shell [class*=\"_body\"]{max-width:100%;overflow-wrap:anywhere}\n@media(max-width:420px){.dshm-shell [data-context-fields]>*{display:grid;grid-template-columns:1fr!important;gap:4px}.dshm-shell [class*=\"_ioSection\"]{grid-template-columns:1fr!important}}\n@media(max-width:600px){\n.dshm-shell [data-question-key],.dshm-shell [data-plan-review-key]{box-sizing:border-box;width:100%;height:auto!important;min-width:0;flex:none!important;align-self:flex-end;padding:6px max(10px,env(safe-area-inset-left)) max(10px,env(safe-area-inset-bottom)) max(10px,env(safe-area-inset-right))!important}\n.dshm-shell [data-question-key]>section,.dshm-shell [data-plan-review-key]>section{width:100%;height:auto!important;min-height:0!important;max-width:none!important;max-height:min(68dvh,520px)!important;border-radius:16px!important}\n.dshm-shell [data-question-scroll],.dshm-shell [data-plan-review-scroll]{flex:0 1 auto!important;min-height:0!important;max-height:min(42dvh,360px)!important;overscroll-behavior:contain;scroll-padding-bottom:12px}\n.dshm-shell [data-question-key]>section>header{display:flex!important;visibility:visible!important;flex:none!important;gap:8px!important;padding:12px 8px 4px 14px!important}\n.dshm-shell [data-question-key]>section>header h2{min-width:0;overflow-wrap:anywhere;font-size:16px!important;line-height:22px!important}\n.dshm-shell [data-question-key]>section>header button{min-width:40px;min-height:40px}\n.dshm-shell [data-question-key] [role=radio],.dshm-shell [data-question-key] [role=checkbox]{min-height:48px!important;touch-action:manipulation}\n.dshm-shell [data-question-key]>section>footer{display:grid!important;grid-template-columns:auto minmax(0,1fr);align-items:center!important;flex:none!important;gap:6px 8px!important;margin-top:4px!important;padding:6px 10px 10px!important}\n.dshm-shell [data-question-key]>section>footer>:last-child{grid-column:1/-1;display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));width:100%;gap:8px!important}\n.dshm-shell [data-question-key]>section>footer>:last-child button{width:100%;min-height:44px}\n.dshm-shell [data-plan-review-key]>section>div:last-child{display:grid!important;grid-template-columns:1fr;gap:8px!important;padding:8px 12px 10px!important}\n.dshm-shell [data-plan-review-key]>section>div:last-child>div:last-child{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));width:100%;gap:8px!important}\n.dshm-shell [data-plan-review-key]>section>div:last-child>div:last-child>button:first-child{grid-column:1/-1}\n.dshm-shell [data-plan-review-key]>section>div:last-child button{width:100%;min-height:44px}\n}\n@media(min-width:900px){\n.dshm-shell{grid-template-columns:auto minmax(0,1fr)}\n.dshm-main{grid-area:1/2}\n.dshm-drawer{position:static;grid-area:1/1;box-shadow:none}\n.dshm-drawer[data-open=true]{width:340px;box-shadow:none}\n.dshm-drawer[data-open=false]{width:56px}\n}\n@media(prefers-reduced-motion:reduce){.dshm-drawer,.dshm-details,.dshm-scrim,.dshm-drawer>*,.dshm-main{transition:none!important}}\n`\n\nfunction MobileAppFrame(props: MobileRootProps & {\n readonly controller: MobileLayoutController\n readonly requestDetailsClose: () => void\n}): ReactNode {\n const state = useSyncExternalStore(props.controller.subscribe, props.controller.getSnapshot)\n const suppressKeyboardUntil = useRef(0)\n const suppressComposerUntil = useRef(0)\n const previousPanelId = useRef(state.panelInfo.activePanelId)\n const navigationIme = useRef(new Map<HTMLElement, string | null>())\n const restoreNavigationIme = (): void => {\n for (const [editor, previous] of navigationIme.current) {\n if (editor.getAttribute('inputmode') !== 'none') continue\n if (previous === null) editor.removeAttribute('inputmode')\n else editor.setAttribute('inputmode', previous)\n }\n navigationIme.current.clear()\n }\n useLayoutEffect(() => {\n const previous = previousPanelId.current\n previousPanelId.current = state.panelInfo.activePanelId\n if (previous === null || state.panelInfo.activePanelId !== null\n || !window.matchMedia(TOUCH_PRIMARY_QUERY).matches) return\n // All panel exits precede the remounted composer's passive autofocus.\n restoreNavigationIme()\n suppressComposerUntil.current = performance.now() + 500\n const active = document.activeElement\n if (active instanceof HTMLElement && active.matches(COMPOSER_EDITOR_SELECTOR)) active.blur()\n }, [state.panelInfo.activePanelId])\n const [viewportWidth, setViewportWidth] = useState(window.innerWidth)\n useEffect(() => {\n let frame: number | undefined\n const resize = (): void => {\n if (frame !== undefined) return\n frame = window.requestAnimationFrame(() => {\n frame = undefined\n setViewportWidth(window.innerWidth)\n })\n }\n window.addEventListener('resize', resize)\n return () => {\n window.removeEventListener('resize', resize)\n if (frame !== undefined) window.cancelAnimationFrame(frame)\n }\n }, [])\n const wideViewport = isWideViewportLayout(viewportWidth)\n const rightbar = resolveMobileRightbarLayout(\n viewportWidth,\n state.sidebarOpen,\n state.rightbarTrack,\n state.rightbarFullscreen,\n )\n const rightbarDocked = state.detailsOpen && rightbar.docked\n const rightbarFullWidth = state.detailsOpen && !rightbarDocked && !wideViewport\n const [documentLanguage, setDocumentLanguage] = useState(document.documentElement.lang)\n const browserLanguages = navigator.languages.length > 0 ? navigator.languages : [navigator.language]\n const language = resolveMobileLayoutLanguage(documentLanguage, browserLanguages)\n const messages = MOBILE_LAYOUT_MESSAGES[language]\n const scrimOpen = !rightbarFullWidth && isMobileScrimOpen(state.sidebarOpen, state.detailsOpen && !rightbarDocked, wideViewport)\n const activeSessionId = props.useSessions(session => {\n const current = session.current\n return current !== undefined && session.byId[current]?.blank === false ? current : undefined\n })\n const hasSession = activeSessionId !== undefined\n\n useEffect(() => {\n const observer = new MutationObserver(() => { setDocumentLanguage(document.documentElement.lang) })\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })\n return () => { observer.disconnect() }\n }, [])\n\n useEffect(() => {\n if (!hasSession) props.controller.closeDetails()\n }, [hasSession, props.controller])\n\n // Mirror the stock layout: project the current session title into the\n // browser tab, restoring the product title when no session is selected.\n const sessionTitle = props.useSessions(session =>\n session.current === undefined ? undefined : session.byId[session.current]?.title,\n )\n useEffect(() => {\n const productTitle = document.title\n if (sessionTitle !== undefined) document.title = `${sessionTitle} — ${productTitle}`\n return () => { document.title = productTitle }\n }, [sessionTitle])\n\n useEffect(() => {\n const elementTarget = (target: EventTarget | null): Element | null =>\n target instanceof Element ? target : null\n const suppressAutofocus = (event: FocusEvent): void => {\n const target = elementTarget(event.target)\n if (target === null) return\n const editable = target instanceof HTMLElement && (target.matches('input,textarea') || target.isContentEditable)\n if (!editable) return\n if (isComposerOwnedFocus(target)) {\n if (performance.now() < suppressComposerUntil.current) target.blur()\n return\n }\n if (performance.now() >= suppressKeyboardUntil.current) return\n target.blur()\n }\n const suppressBranchAutofocus = (event: MouseEvent): void => {\n if (!(event.target instanceof Element)) return\n const branch = event.target.closest('button[aria-label*=\"分支\"],button[aria-label*=\"Branch\"],button[aria-label*=\"branch\"],button[aria-label*=\"Ramo\"],button[aria-label*=\"ramo\"]')\n if (branch === null || branch.hasAttribute('disabled') || branch.getAttribute('aria-disabled') === 'true') return\n suppressKeyboardUntil.current = performance.now() + 700\n window.setTimeout(() => {\n const active = document.activeElement\n if (active instanceof HTMLElement && (active.matches('input,textarea') || active.isContentEditable)) active.blur()\n }, 0)\n }\n // The native composer deliberately preserves editor focus when its Add menu\n // opens, and on a phone that focus summons the soft keyboard. Withholding the\n // IME leaves the focus, the caret, and the draft exactly as stock left them;\n // any other tap restores the keyboard.\n const applyComposerImePolicy = (event: PointerEvent): void => {\n const target = elementTarget(event.target)\n // A deliberate tap in the composer ends the navigation-only suppression\n // before focus can reach the editor. The Add trigger then reclaims its\n // existing inputmode=\"none\" policy below.\n if (isComposerOwnedFocus(target)) {\n suppressComposerUntil.current = 0\n restoreNavigationIme()\n }\n const policy = resolveComposerImePolicy(\n target,\n window.matchMedia(TOUCH_PRIMARY_QUERY).matches,\n )\n if (policy === 'ignore') return\n for (const editor of document.querySelectorAll<HTMLElement>(COMPOSER_EDITOR_SELECTOR)) {\n if (policy === 'withhold') editor.setAttribute('inputmode', 'none')\n else if (editor.getAttribute('inputmode') === 'none') editor.removeAttribute('inputmode')\n }\n }\n document.addEventListener('focusin', suppressAutofocus, true)\n document.addEventListener('click', suppressBranchAutofocus, true)\n // A finger tap never fires mousedown on Android, and this has to land before\n // the trigger's own focus, so it runs on capture for pointerdown.\n document.addEventListener('pointerdown', applyComposerImePolicy, true)\n const disposeSendKeyboard = installComposerSendKeyboardPolicy(() => {\n suppressComposerUntil.current = performance.now() + 700\n })\n return () => {\n document.removeEventListener('focusin', suppressAutofocus, true)\n document.removeEventListener('click', suppressBranchAutofocus, true)\n document.removeEventListener('pointerdown', applyComposerImePolicy, true)\n disposeSendKeyboard()\n restoreNavigationIme()\n // A withheld editor outliving the surface would strand the composer without\n // a keyboard for the rest of the session.\n for (const editor of document.querySelectorAll<HTMLElement>(`${COMPOSER_CARD_SELECTOR} [inputmode=\"none\"]`)) {\n editor.removeAttribute('inputmode')\n }\n }\n }, [])\n\n // Session navigation on touch withholds upstream composer autofocus; only a\n // narrow overlay drawer closes after the row's own click handler completes.\n const closeDrawerAfterSessionAction = (event: { readonly target: EventTarget | null }): void => {\n if (!(event.target instanceof Element)) return\n if (isSessionRowNavigation(event.target) && window.matchMedia(TOUCH_PRIMARY_QUERY).matches) {\n const deadline = performance.now() + 500\n suppressComposerUntil.current = deadline\n for (const editor of document.querySelectorAll<HTMLElement>(COMPOSER_EDITOR_SELECTOR)) {\n if (!navigationIme.current.has(editor)) navigationIme.current.set(editor, editor.getAttribute('inputmode'))\n editor.setAttribute('inputmode', 'none')\n if (document.activeElement === editor) editor.blur()\n }\n window.setTimeout(() => {\n if (suppressComposerUntil.current !== deadline) return\n suppressComposerUntil.current = 0\n restoreNavigationIme()\n }, 500)\n }\n if (viewportIsWide()) return\n const row = event.target.closest<HTMLElement>('[role=\"treeitem\"][aria-selected]')\n const action = event.target.closest('button,[role=\"button\"]')\n const startsSession = action?.matches('button[class*=\"_newSession\"],button[class*=\"_brand\"]')\n || /新建会话|新会话|new session|new conversation|nuova sessione|nuova conversazione/i.test(action?.getAttribute('aria-label') ?? '')\n if (row === null && !startsSession) return\n if (row !== null && action !== null && action !== row) return\n suppressKeyboardUntil.current = performance.now() + 500\n // Let the session row finish its own click handler before unmounting the drawer.\n window.setTimeout(() => {\n props.controller.closeSidebar()\n const active = document.activeElement\n if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) active.blur()\n }, 0)\n }\n\n return createElement('div', {\n className: 'dshm-shell', lang: language,\n 'data-rightbar-docked': rightbarDocked,\n style: { '--dshm-rightbar-width': `${rightbar.width}px` },\n },\n createElement('main', {\n className: 'dshm-main',\n 'data-dsh-mobile-session': activeSessionId,\n ...(rightbarFullWidth ? { inert: '', 'aria-hidden': true } : {}),\n // Legacy dsh-web community plugins use semantic pane attributes as their\n // compatibility path when the stock CSS-module column names are absent.\n 'data-pane': 'conversation',\n },\n state.panelInfo.activePanelId !== null && !wideViewport ? createElement('div', {\n className: 'dshm-panelNav',\n }, createElement('button', {\n type: 'button',\n className: 'dshm-panelBack',\n 'aria-label': messages.backToConversation,\n disabled: state.sidebarOpen,\n onClick: () => { props.controller.selectPanel(null) },\n }, createElement('svg', {\n 'aria-hidden': true, viewBox: '0 0 24 24', fill: 'none',\n }, createElement('path', {\n d: 'M15 5l-7 7 7 7',\n stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n })), createElement('span', null, messages.backToConversation))) : null,\n createElement('div', { className: 'dshm-mainContent' }, props.renderSlot('main', {}, {\n entryKey: state.panelInfo.activePanelId ?? 'conversation',\n fallback: props.renderSlot('conversation', {}),\n }))),\n createElement('button', {\n 'aria-label': messages.closePanels,\n className: 'dshm-scrim',\n // A persistent wide sidebar needs no dimming; the scrim only covers\n // the narrow overlay drawer and the details panel.\n 'data-open': scrimOpen,\n 'aria-hidden': !scrimOpen,\n onClick: () => {\n if (!scrimOpen) return\n state.detailsOpen ? props.requestDetailsClose() : props.controller.closeSidebar()\n },\n tabIndex: scrimOpen ? 0 : -1,\n type: 'button',\n }),\n createElement('aside', {\n 'aria-label': messages.workspaceNavigation,\n className: 'dshm-drawer',\n 'data-pane': 'sidebar',\n 'data-open': state.sidebarOpen,\n // The phone adapter promotes the whale toggle to a fixed high layer.\n // Remove that entire navigation surface while the right modal owns focus.\n 'data-right-modal': state.detailsOpen && !rightbarDocked,\n ...(state.detailsOpen && !rightbarDocked ? { inert: '', 'aria-hidden': true } : {}),\n // Community rows collapse their label when an ancestor carries this marker.\n ...(state.sidebarOpen ? {} : { 'data-sidebar-collapsed': '' }),\n onClickCapture: closeDrawerAfterSessionAction,\n }, props.renderSlot('sidebar', {\n collapsed: !state.sidebarOpen,\n width: state.sidebarOpen ? 340 : 56,\n })),\n createElement('aside', {\n 'aria-hidden': !state.detailsOpen,\n className: 'dshm-details',\n 'data-open': state.detailsOpen,\n ...(state.detailsOpen ? {} : { inert: '' }),\n }, [\n // DSH 0.1.3 and earlier bind details to a Session. Newer DSH owns the\n // Session decision inside its root-scoped rightbar occupant, so that\n // occupant must stay mounted even while no Session is selected.\n hasSession ? props.renderSlot('details', {}) : undefined,\n props.renderSlot('rightbar', {\n width: rightbar.width,\n viewportWidth,\n // The official frame reports whether a persistent column still fits\n // (normal.rightbar > 0); this layout renders the right panel in the\n // details overlay, which needs no column space. Reporting false would\n // make RightbarSeat force the surface closed immediately after opening\n // it (ui-sidebar-right: `shown && !fullscreen && !canShow` ->\n // actions.setExpanded(false)), so the panel could never stay open on a\n // phone-width viewport.\n canShow: true,\n }),\n ]),\n createElement('div', { className: 'dshm-overlay', 'data-shell-overlay': true }, props.renderSlot('shell.overlay', {})),\n )\n}\n\n// One controller per document: re-applying the layout (reconnect, tab\n// refocus) must not reset the user's explicit sidebar toggle.\nlet sharedController: MobileLayoutController | undefined\n\n/** Replace the desktop layout module on the authenticated mobile surface. */\nexport function apply(ctx: MobileClientContext): void {\n sharedController ??= new MobileLayoutController()\n const controller = sharedController\n ctx.effect(() => {\n const style = document.createElement('style')\n style.dataset.plugin = 'dsh-mobile-layout'\n style.textContent = MOBILE_LAYOUT_STYLES\n document.head.append(style)\n const disposeMainPanelRegistry = controller.bindMainPanelRegistry(id =>\n ctx.slots.entries('main').some(entry => entry.options.key === id))\n const disposeService = ctx.reflect.provide('layout', controller)\n // The official layout owns the root `panelInfo` hook; replacing the layout\n // module means replacing that owner. Without it every registration that\n // declares the standard `usePanelInfo` prop fails assembly\n // (\"strict standard hook 'panelInfo' has no source\") and renders as a dead\n // cell — the workspace sidebar's session list lives exactly there.\n const disposePanelInfo = typeof ctx.slots.provideRoot === 'function'\n ? ctx.slots.provideRoot({ hooks: { panelInfo: controller.panelInfo } })\n : () => {}\n const requestDetailsClose = (): void => {\n closeDetailsFromScrim(ctx.get('sidebarRight'), () => { controller.closeDetails() })\n }\n const disposeRoot = ctx.slots.register<MobileRootProps>({\n name: 'root',\n children: {\n sidebar: { kind: 'single', scope: 'root' },\n conversation: { kind: 'single', scope: 'session-maybe' },\n // The conversation panel moved into this keyed slot in DSH 0.1.5;\n // without the declaration the center column stays blank.\n main: { kind: 'keyed', scope: 'root' },\n rightbar: { kind: 'single', scope: 'root' },\n details: { kind: 'single', scope: 'session' },\n 'shell.overlay': { kind: 'list', scope: 'root' },\n },\n }, props => createElement(MobileAppFrame, {\n ...props,\n controller,\n // sidebarRight is registered after the layout it depends on, so resolve\n // the optional service at gesture time rather than caching it at boot.\n requestDetailsClose,\n }))\n const onNativeBack = (event: Event): void => {\n if (!event.cancelable || event.defaultPrevented) return\n const state = controller.getSnapshot()\n if (state.detailsOpen) {\n event.preventDefault()\n requestDetailsClose()\n } else if (!viewportIsWide() && state.sidebarOpen) {\n event.preventDefault()\n controller.closeSidebar()\n } else if (state.panelInfo.activePanelId !== null) {\n event.preventDefault()\n controller.selectPanel(null)\n }\n }\n window.addEventListener('dsh-mobile:native-back', onNativeBack)\n const retainMainPanels = (): void => {\n controller.retainMainPanels(ctx.slots.entries('main').flatMap(entry =>\n entry.options.key === undefined ? [] : [entry.options.key]))\n }\n const disposePanels = ctx.slots.subscribe('main', retainMainPanels)\n retainMainPanels()\n return () => {\n window.removeEventListener('dsh-mobile:native-back', onNativeBack)\n controller.dispose()\n disposePanels()\n disposeRoot()\n disposePanelInfo()\n void disposeService()\n disposeMainPanelRegistry()\n style.remove()\n }\n }, 'dsh-mobile: dedicated root layout')\n\n ctx.effect(() => {\n const presenter = new ThemePresenter()\n presenter.apply(ctx.theme.getTheme())\n const off = ctx.on('theme/change', snapshot => { presenter.apply(snapshot) })\n return () => { off(); presenter.dispose() }\n }, 'dsh-mobile: theme presenter')\n}\n\n/** Preserve the official layout module's dependency ordering. */\nexport const inject: readonly string[] = ['slots', 'theme']\n"],"mappings":"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"}
1
+ {"version":3,"file":"mobile-layout.js","names":["useSyncExternalStore","useRef","useState","createElement"],"sources":["../src/mobile-layout-messages.ts","../src/mobile-layout.ts"],"sourcesContent":["export type MobileLayoutLanguage = 'it' | 'en' | 'zh'\n\nexport const MOBILE_LAYOUT_MESSAGES = Object.freeze({\n it: Object.freeze({ closePanels: 'Chiudi pannelli', workspaceNavigation: 'Navigazione area di lavoro e sessioni', backToConversation: 'Torna alla conversazione' }),\n en: Object.freeze({ closePanels: 'Close panels', workspaceNavigation: 'Workspace and session navigation', backToConversation: 'Back to conversation' }),\n zh: Object.freeze({ closePanels: '关闭浮层', workspaceNavigation: '工作区与会话导航', backToConversation: '返回会话' }),\n})\n","import { createElement, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport type { ReactNode } from 'react'\nimport { MOBILE_LAYOUT_MESSAGES, type MobileLayoutLanguage } from './mobile-layout-messages.js'\n\nexport { MOBILE_LAYOUT_MESSAGES } from './mobile-layout-messages.js'\nexport type { MobileLayoutLanguage } from './mobile-layout-messages.js'\n\ninterface SessionState {\n readonly current?: string\n readonly byId: Readonly<Record<string, { readonly blank?: boolean; readonly title?: string } | undefined>>\n}\n\ninterface MobileRootProps {\n readonly renderSlot: (\n name: string,\n owner: Record<string, unknown>,\n options?: { readonly entryKey?: string | null; readonly fallback?: ReactNode },\n ) => ReactNode\n readonly useSessions: <T>(selector: (state: SessionState) => T) => T\n}\n\n/** Root contribution carrying one observable hook into the standard kit. */\ninterface RootHookContribution {\n readonly hooks: Readonly<Record<string, {\n readonly getSnapshot: () => unknown\n readonly subscribe: (listener: () => void) => () => void\n }>>\n}\n\ninterface MobileClientContext {\n readonly effect: (effect: () => void | (() => void), label?: string) => void\n readonly get: (name: string) => unknown\n readonly on: (event: string, listener: (value: ThemeSnapshot) => void) => () => void\n readonly reflect: { provide: (name: string, value: unknown) => () => void | Promise<void> }\n readonly slots: {\n register: <Props>(options: Record<string, unknown>, component: (props: Props) => ReactNode) => () => void\n entries: (name: string) => readonly { readonly options: { readonly key?: string } }[]\n subscribe: (name: string, listener: () => void) => () => void\n /**\n * Publish a root standard hook. The official layout owns `panelInfo`, so\n * replacing that module means replacing the owner too.\n */\n provideRoot?: (contribution: RootHookContribution) => () => void\n }\n readonly theme: { getTheme: () => ThemeSnapshot }\n}\n\ninterface ThemeSnapshot {\n readonly active: {\n readonly colorScheme: 'dark' | 'light'\n readonly tokens: Readonly<Record<string, string>>\n }\n}\n\ninterface LayoutSnapshot {\n readonly sidebarOpen: boolean\n readonly detailsOpen: boolean\n readonly rightbarTrack: boolean\n readonly rightbarFullscreen: boolean\n readonly panelInfo: PanelInfoSnapshot\n}\n\n/**\n * Panel selection as the standard `usePanelInfo` hook reports it: a null\n * `activePanelId` means the conversation owns the center column.\n */\ninterface PanelInfoSnapshot {\n readonly activePanelId: string | null\n}\n\n/** Optional DSH 0.1.5 right-Sidebar controls used by a Mobile close gesture. */\nexport interface SidebarRightControl {\n isExpanded(): boolean\n toggleExpanded(): void\n}\n\n/** Whether an optional service value supports the public right-Sidebar controls. */\nexport function isSidebarRightControl(value: unknown): value is SidebarRightControl {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as Partial<SidebarRightControl>\n return typeof candidate.isExpanded === 'function' && typeof candidate.toggleExpanded === 'function'\n}\n\n/**\n * Viewport width at which the dedicated layout treats the sidebar as a\n * persistent desktop panel instead of an overlay drawer. Narrow screens\n * keep the overlay behavior byte-for-byte.\n */\nexport const WIDE_LAYOUT_MIN_WIDTH_PX = 900\nconst RIGHTBAR_MIN_WIDTH_PX = 300\nconst RIGHTBAR_MAX_WIDTH_PX = 460\nconst RIGHTBAR_CENTER_MIN_WIDTH_PX = 400\nconst RIGHTBAR_DEFAULT_RATIO = 0.45\nconst SIDEBAR_EXPANDED_WIDTH_PX = 340\nconst SIDEBAR_COLLAPSED_WIDTH_PX = 56\n\nexport function isWideViewportLayout(viewportWidth: number): boolean {\n return viewportWidth >= WIDE_LAYOUT_MIN_WIDTH_PX\n}\n\n/** Resolved right-panel presentation for the dedicated responsive layout. */\nexport interface MobileRightbarLayout {\n readonly docked: boolean\n readonly width: number\n}\n\n/** Preserve a readable conversation width before turning a requested right track into a docked column. */\nexport function resolveMobileRightbarLayout(\n viewportWidth: number,\n sidebarOpen: boolean,\n track: boolean,\n fullscreen: boolean,\n): MobileRightbarLayout {\n const overlayWidth = isWideViewportLayout(viewportWidth)\n ? Math.min(viewportWidth * 0.94, RIGHTBAR_MAX_WIDTH_PX)\n : viewportWidth\n if (!track || fullscreen || (!isWideViewportLayout(viewportWidth) && sidebarOpen)) {\n return Object.freeze({ docked: false, width: overlayWidth })\n }\n const sidebarWidth = isWideViewportLayout(viewportWidth) && sidebarOpen\n ? SIDEBAR_EXPANDED_WIDTH_PX\n : SIDEBAR_COLLAPSED_WIDTH_PX\n const available = Math.floor(viewportWidth - sidebarWidth - RIGHTBAR_CENTER_MIN_WIDTH_PX)\n if (available < RIGHTBAR_MIN_WIDTH_PX) return Object.freeze({ docked: false, width: overlayWidth })\n const preferred = Math.min(\n RIGHTBAR_MAX_WIDTH_PX,\n Math.max(RIGHTBAR_MIN_WIDTH_PX, Math.round(viewportWidth * RIGHTBAR_DEFAULT_RATIO)),\n )\n return Object.freeze({ docked: true, width: Math.min(preferred, available) })\n}\n\n/** Whether an overlay drawer needs the modal scrim without dimming a docked desktop panel. */\nexport function isMobileScrimOpen(\n sidebarOpen: boolean,\n detailsModalOpen: boolean,\n wideViewport: boolean,\n): boolean {\n return (!wideViewport && sidebarOpen) || detailsModalOpen\n}\n\n/**\n * Query matching devices whose primary pointer cannot hover: the phones and\n * tablets where focusing an editor raises a soft keyboard the user did not ask\n * for. A desktop keeps stock behaviour because it has a real keyboard already.\n */\nexport const TOUCH_PRIMARY_QUERY = '(hover: none), (pointer: coarse)'\n\n/** The stock composer wrapper, which owns everything the phone adapter edits. */\nconst COMPOSER_CARD_SELECTOR = '[data-composer-card]'\n\n/** The composer's editing host, whichever contenteditable flavour it boots with. */\nconst COMPOSER_EDITOR_SELECTOR = `${COMPOSER_CARD_SELECTOR} :is([contenteditable=\"true\"],[contenteditable=\"plaintext-only\"],[contenteditable=\"\"])`\n\n/** The composer's Add trigger, whose menu must open without summoning the IME. */\nconst COMPOSER_COMMAND_TRIGGER_SELECTOR = `${COMPOSER_CARD_SELECTOR} button[aria-haspopup=\"listbox\"]`\n\n/**\n * Whether a focus target belongs to the composer, which owns its own keyboard\n * decision and must never be blurred by a suppression window.\n * @param target - the focus target, already narrowed to an element.\n * @returns whether the target sits inside the composer card.\n */\nexport function isComposerOwnedFocus(target: Element | null): boolean {\n return target !== null && target.closest(COMPOSER_CARD_SELECTOR) !== null\n}\n\n/**\n * Whether a click opens a different Session, rather than its menu or current row.\n * @param target - the click target in the workspace sidebar.\n * @returns whether the Session selection can change.\n */\nexport function isSessionRowNavigation(target: Element | null): boolean {\n if (target === null) return false\n const row = target.closest('[role=\"treeitem\"][aria-selected]')\n if (row === null || row.getAttribute('aria-selected') === 'true') return false\n const action = target.closest('button,[role=\"button\"]')\n return action === null || action === row\n}\n\n/**\n * Decide how one pointer event changes the composer's soft keyboard.\n *\n * The Add button's own mousedown calls `keepDraftFocus`, which focuses the draft\n * on purpose so the caret survives the menu; on a phone that focus is what\n * raises the keyboard. `inputmode=\"none\"` withholds the IME without moving focus\n * or the caret, so the menu, the caret, and the draft all keep stock behaviour.\n * @param target - the pointer target, already narrowed to an element.\n * @param touchPrimary - whether the device's primary pointer cannot hover.\n * @returns `withhold` for the Add trigger, `restore` for any other tap, and\n * `ignore` where no soft keyboard exists to withhold.\n */\nexport function resolveComposerImePolicy(\n target: Element | null,\n touchPrimary: boolean,\n): 'ignore' | 'restore' | 'withhold' {\n if (!touchPrimary || target === null) return 'ignore'\n return target.closest(COMPOSER_COMMAND_TRIGGER_SELECTOR) === null ? 'restore' : 'withhold'\n}\n\n/**\n * Whether a tap landed on the stock Send, Queue, or Steer primary button.\n * Stop uses a square SVG; Add, attachment, and plugin buttons are separate actions.\n * @param target - the click target in the composer card.\n * @returns whether the enabled primary action carries the send arrow.\n */\nexport function isComposerActionTap(target: Element | null): boolean {\n if (target === null) return false\n const action = target.closest('button[class*=\"_primary\"]')\n return action !== null && action.closest(COMPOSER_CARD_SELECTOR) !== null\n && !action.hasAttribute('disabled') && action.getAttribute('aria-disabled') !== 'true'\n && action.querySelector('svg path') !== null\n}\n\n/** Text, reference chips, and the stock attachment rail all contribute to a draft. */\nfunction composerHasDraft(editor: HTMLElement, card: Element): boolean {\n return (editor.textContent ?? '').replaceAll('\\u200b', '').trim() !== ''\n || editor.querySelector('[contenteditable=\"false\"],[data-lexical-decorator],img') !== null\n || card.querySelector('[data-slot=\"conversation.input.attachments\"] [role=\"group\"] > *') !== null\n}\n\n/**\n * Collapse an on-screen keyboard after a trusted touch Send consumes the focused\n * draft. Consumption may be asynchronous; input, navigation, composition, or\n * another gesture cancels the observation. DSH owns optimistic sends and any\n * later failure restoration; this listener never submits or modifies the draft.\n * @param onConsumed - called immediately before blurring to suppress DSH autofocus.\n * @returns disposer for pointer, composition, focus, and draft observations.\n */\nexport function installComposerSendKeyboardPolicy(onConsumed: () => void): () => void {\n let touchButton: Element | null = null\n let cancelPending = (): void => {}\n const composing = new WeakSet<Element>()\n let compositionEndedAt = -Infinity\n const elementTarget = (target: EventTarget | null): Element | null => target instanceof Element ? target : null\n const onPointerDown = (event: PointerEvent): void => {\n cancelPending()\n const target = elementTarget(event.target)\n touchButton = event.isTrusted && (event.pointerType === 'touch' || event.pointerType === 'pen')\n && isComposerActionTap(target) ? target?.closest('button') ?? null : null\n }\n const onClick = (event: MouseEvent): void => {\n const action = touchButton\n touchButton = null\n const target = elementTarget(event.target)\n const active = document.activeElement\n const nativeState = window.__DSH_MOBILE_KEYBOARD_STATE__\n if (!event.isTrusted || event.detail === 0 || action === null || target?.closest('button') !== action\n || !isComposerActionTap(target) || !window.matchMedia(TOUCH_PRIMARY_QUERY).matches\n || (window.__DSH_MOBILE_NATIVE__ !== undefined\n && (nativeState?.noHardwareKeyboard !== true || nativeState.imeVisible !== true))\n || !(active instanceof HTMLElement) || !active.matches(COMPOSER_EDITOR_SELECTOR)\n || composing.has(active) || performance.now() - compositionEndedAt < 10) return\n const editor = active\n const composerCard = editor.closest(COMPOSER_CARD_SELECTOR)\n if (composerCard === null || action.closest(COMPOSER_CARD_SELECTOR) !== composerCard || !composerHasDraft(editor, composerCard)) return\n const card = composerCard\n const sessionOwner = card.closest('[data-conversation-session]')\n const sessionId = sessionOwner?.getAttribute('data-conversation-session')\n const currentEditor = (): boolean => editor.isConnected && editor.closest(COMPOSER_CARD_SELECTOR) === card\n && card.closest('[data-conversation-session]') === sessionOwner\n && sessionOwner?.getAttribute('data-conversation-session') === sessionId\n && document.activeElement === editor\n cancelPending()\n let frame = 0\n const observer = new MutationObserver(check)\n const expiry = window.setTimeout(() => { cancelPending() }, 30_000)\n cancelPending = (): void => {\n observer.disconnect()\n window.clearTimeout(expiry)\n window.cancelAnimationFrame(frame)\n cancelPending = () => {}\n }\n function check(): void {\n if (!currentEditor()) {\n cancelPending()\n return\n }\n if (composerHasDraft(editor, card) || ['adjudicating', 'submitting'].includes(editor.dataset.phase ?? '')) return\n if (frame !== 0) return\n frame = window.requestAnimationFrame(() => {\n frame = window.requestAnimationFrame(() => {\n frame = 0\n if (!currentEditor() || composerHasDraft(editor, card)\n || ['adjudicating', 'submitting'].includes(editor.dataset.phase ?? '')) return\n cancelPending()\n onConsumed()\n editor.blur()\n })\n })\n }\n observer.observe(card, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['data-phase', 'contenteditable'] })\n // The capture listener runs before React's onClick; the first frame reads its result.\n frame = window.requestAnimationFrame(() => { frame = 0; check() })\n }\n const onCompositionStart = (event: CompositionEvent): void => {\n const target = elementTarget(event.target)\n if (target !== null) composing.add(target)\n cancelPending()\n }\n const onCompositionEnd = (event: CompositionEvent): void => {\n const target = elementTarget(event.target)\n if (target !== null) composing.delete(target)\n compositionEndedAt = performance.now()\n }\n const onTyping = (): void => { touchButton = null; cancelPending() }\n const onFocus = (): void => { cancelPending() }\n document.addEventListener('pointerdown', onPointerDown, true)\n document.addEventListener('click', onClick, true)\n document.addEventListener('compositionstart', onCompositionStart, true)\n document.addEventListener('compositionend', onCompositionEnd, true)\n document.addEventListener('keydown', onTyping, true)\n document.addEventListener('input', onTyping, true)\n document.addEventListener('focusin', onFocus, true)\n return () => {\n cancelPending()\n document.removeEventListener('pointerdown', onPointerDown, true)\n document.removeEventListener('click', onClick, true)\n document.removeEventListener('compositionstart', onCompositionStart, true)\n document.removeEventListener('compositionend', onCompositionEnd, true)\n document.removeEventListener('keydown', onTyping, true)\n document.removeEventListener('input', onTyping, true)\n document.removeEventListener('focusin', onFocus, true)\n }\n}\n\n/**\n * Close the details drawer from its scrim, collapsing the DSH 0.1.5 right\n * Sidebar first when that optional service owns the visible content.\n */\nexport function closeDetailsFromScrim(sidebarRight: unknown, closeDetails: () => void): void {\n if (isSidebarRightControl(sidebarRight) && sidebarRight.isExpanded()) {\n sidebarRight.toggleExpanded()\n }\n closeDetails()\n}\n\n/** Reads the live viewport; unknown environments (SSR, tests) stay narrow. */\nfunction viewportIsWide(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false\n return window.matchMedia(`(min-width: ${WIDE_LAYOUT_MIN_WIDTH_PX}px)`).matches\n}\n\n/** Resolve the supported language used by the dedicated mobile layout. */\nexport function resolveMobileLayoutLanguage(\n documentLanguage: string,\n browserLanguages: readonly string[],\n): MobileLayoutLanguage {\n return [documentLanguage, ...browserLanguages]\n .map(value => value.trim().toLowerCase().split(/[-_]/u)[0])\n .find((value): value is MobileLayoutLanguage => value === 'it' || value === 'en' || value === 'zh') ?? 'en'\n}\n\nclass MobileLayoutController {\n // Wide viewports start with the persistent sidebar open; applying the\n // layout again (reconnect, refocus) reuses the module singleton below,\n // so an explicit user collapse is never reset.\n private snapshot: LayoutSnapshot = Object.freeze({\n sidebarOpen: viewportIsWide(),\n detailsOpen: false,\n rightbarTrack: false,\n rightbarFullscreen: false,\n panelInfo: Object.freeze({ activePanelId: null }),\n })\n private readonly listeners = new Set<() => void>()\n private hasMainPanel: ((id: string) => boolean) | undefined\n // Pending workspace/session navigation, cancelled whenever a newer one\n // starts. Mirrors the official LayoutController's navigation controller.\n private navigation = new AbortController()\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n readonly getSnapshot = (): LayoutSnapshot => this.snapshot\n\n /**\n * The panel store on the layout service itself (`ctx.layout.panelInfo`),\n * mirroring the official LayoutController of DSH 0.1.7-rc.2+. The root hook\n * wired in `apply` feeds `usePanelInfo` rendering; rc.2 client plugins read\n * this service member directly. Without it `dsh-client-ui-plugin-manager`\n * fails activation on the dedicated mobile frontend (\"web boot: 1 entry did\n * not activate\") and `dsh-client-ui-open-in-app` throws the first time its\n * shortcut target runs.\n */\n readonly panelInfo = {\n getSnapshot: (): PanelInfoSnapshot => this.getSnapshot().panelInfo,\n subscribe: (listener: () => void): (() => void) => this.subscribe(listener),\n }\n\n toggleSidebar(): void {\n this.update({ sidebarOpen: !this.snapshot.sidebarOpen })\n }\n\n openDetails(): void {\n this.update({ detailsOpen: true })\n }\n\n closeDetails(): void {\n this.update({ detailsOpen: false, rightbarTrack: false, rightbarFullscreen: false })\n }\n\n closeSidebar(): void {\n this.update({ sidebarOpen: false })\n }\n\n /**\n * Invalidate the previous navigation and hand back a fresh signal. The\n * workspace controller wraps this around \"open a workspace\" and \"fork a\n * session\" (dsh-client-ui-workspace) — the path behind the sidebar's\n * new-session button. Without it startSession throws inside a\n * `.catch(reason => console.warn('new session failed:', reason))`, so the\n * button silently did nothing.\n */\n beginNavigation(): AbortSignal {\n this.navigation.abort()\n this.navigation = new AbortController()\n return this.navigation.signal\n }\n\n /** Invalidate pending navigations when this layout unloads. */\n dispose(): void {\n this.navigation.abort()\n }\n\n /** Bind panel validation to the live slot registry owned by this application. */\n bindMainPanelRegistry(hasMainPanel: (id: string) => boolean): () => void {\n this.hasMainPanel = hasMainPanel\n return () => {\n if (this.hasMainPanel === hasMainPanel) this.hasMainPanel = undefined\n }\n }\n\n /** Preserve the native seat's distinction between a docked track and fullscreen. */\n openRightbar(track = true, fullscreen = false): void {\n this.update({ detailsOpen: true, rightbarTrack: track, rightbarFullscreen: fullscreen })\n }\n\n /** Hide the right panel. */\n closeRightbar(): void {\n this.closeDetails()\n }\n\n /**\n * Select the main panel rendered in the center column; null means the\n * conversation. Mirrors the official LayoutController so the `panelInfo`\n * root hook stays truthful. Selecting a panel while the narrow overlay\n * drawer covers the center column dismisses that drawer — the user asked to\n * see the panel, not the navigation over it — while a docked wide sidebar\n * stays open.\n */\n selectPanel(panelId: string | null): void {\n if (panelId !== null && this.hasMainPanel?.(panelId) !== true) {\n throw new Error(`layout.selectPanel: main panel \"${panelId}\" is not registered`)\n }\n this.navigation.abort()\n const dismissDrawer = this.snapshot.sidebarOpen && !viewportIsWide()\n const samePanel = this.snapshot.panelInfo.activePanelId === panelId\n if (!dismissDrawer && samePanel) return\n this.update({\n ...(dismissDrawer ? { sidebarOpen: false } : {}),\n ...(samePanel ? {} : { panelInfo: Object.freeze({ activePanelId: panelId }) }),\n })\n }\n\n /** Drop a selected panel once no main-slot entry declares it. */\n retainMainPanels(panelIds: readonly string[]): void {\n const active = this.snapshot.panelInfo.activePanelId\n if (active !== null && !panelIds.includes(active)) {\n this.update({ panelInfo: Object.freeze({ activePanelId: null }) })\n }\n }\n\n private update(next: Partial<LayoutSnapshot>): void {\n const snapshot = Object.freeze({ ...this.snapshot, ...next })\n if (\n snapshot.sidebarOpen === this.snapshot.sidebarOpen\n && snapshot.detailsOpen === this.snapshot.detailsOpen\n && snapshot.rightbarTrack === this.snapshot.rightbarTrack\n && snapshot.rightbarFullscreen === this.snapshot.rightbarFullscreen\n && snapshot.panelInfo === this.snapshot.panelInfo\n ) return\n this.snapshot = snapshot\n for (const listener of this.listeners) listener()\n }\n}\n\nclass ThemePresenter {\n private appliedTokens: string[] = []\n private readonly meta = document.createElement('meta')\n\n constructor() {\n this.meta.name = 'theme-color'\n }\n\n apply(snapshot: ThemeSnapshot): void {\n const scheme = snapshot.active.colorScheme\n document.documentElement.style.colorScheme = scheme\n document.body.toggleAttribute('data-ds-dark-theme', scheme === 'dark')\n for (const name of this.appliedTokens) document.body.style.removeProperty(name)\n this.appliedTokens = []\n for (const [name, value] of Object.entries(snapshot.active.tokens)) {\n document.body.style.setProperty(name, value)\n this.appliedTokens.push(name)\n }\n this.meta.content = getComputedStyle(document.body).backgroundColor\n if (!this.meta.isConnected) document.head.append(this.meta)\n }\n\n dispose(): void {\n document.documentElement.style.removeProperty('color-scheme')\n document.body.removeAttribute('data-ds-dark-theme')\n for (const name of this.appliedTokens) document.body.style.removeProperty(name)\n this.meta.remove()\n }\n}\n\nexport const MOBILE_LAYOUT_STYLES = `\nhtml,body,#root{width:100%;height:100%;overflow:hidden}\n/* Definite height bounds the transcript on engines without dvh. Isolation keeps\n shell descendants below positive-z-index portals attached to document.body. */\n.dshm-shell{position:relative;isolation:isolate;display:grid;grid-template-rows:minmax(0,1fr);width:100%;height:100%;height:100dvh;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#fff)}\n/* The mobile root owns its Grid geometry. Layered important rules keep broad\n unlayered skin rules from collapsing virtualized views to the header height. */\n@layer dsh-mobile-layout{\n.dshm-shell{display:grid!important;grid-template-rows:minmax(0,1fr)!important;grid-template-columns:minmax(0,1fr)!important}\n@media(min-width:900px){.dshm-shell{grid-template-columns:auto minmax(0,1fr)!important}}\n}\n.dshm-main{grid-area:1/1;position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;margin-right:0;overflow:hidden;transition:margin-right var(--ds-transition-duration-slow,190ms) var(--ds-ease-in-out,ease)}\n.dshm-mainContent{position:relative;flex:1;min-width:0;min-height:0;overflow:hidden}\n.dshm-shell[data-rightbar-docked=true] .dshm-main{margin-right:var(--dshm-rightbar-width)}\n.dshm-shell[data-rightbar-docked=true] .dshm-details{position:absolute;width:var(--dshm-rightbar-width);box-shadow:none;padding-top:0}\n/* Draw above the native absolute panel without consuming its width or\n intercepting controls; only docked mode needs this column separator. */\n.dshm-shell[data-rightbar-docked=true] .dshm-details::after{content:\"\";position:absolute;inset:0 auto 0 0;width:.5px;background:var(--dsw-alias-border-l4);z-index:11;pointer-events:none}\n/* The native panel paints itself at 100vw in fullscreen mode. Inside the\n docked overlay (94vw/460px, overflow:hidden) that layout was clipped to the\n right edge of the track, so the header slid out of view and the body showed\n only clipped line tails. A fullscreen panel owns the whole viewport. */\n.dshm-shell[data-rightbar-fullscreen=true] .dshm-details{inset:0;width:100vw;max-width:100vw;box-shadow:none}\n.dshm-main>*,.dshm-main>*>*{min-width:0}\n.dshm-drawer{position:fixed;z-index:70;inset:0 auto 0 0;box-sizing:border-box;width:56px;max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-layer-1,#f8fafc),var(--dsw-alias-bg-layer-1,#f8fafc));box-shadow:none;will-change:width;transition:width 240ms cubic-bezier(.22,1,.36,1),box-shadow 240ms ease}\n.dshm-drawer[data-open=true]{width:min(88vw,340px);box-shadow:18px 0 46px rgb(15 23 42 / 18%)}\n.dshm-drawer[data-open=false]{pointer-events:auto;visibility:visible}\n.dshm-drawer>*{width:100%!important;height:100%!important;transform:translateX(-6px);opacity:.94;transition:transform 220ms cubic-bezier(.22,1,.36,1),opacity 160ms ease-out}\n.dshm-drawer[data-open=true]>*{transform:translateX(0);opacity:1}\n.dshm-drawer[data-open=false]>*{width:56px!important}\n/* display:none also suppresses fixed descendants installed by the phone adapter. */\n.dshm-drawer[data-right-modal=true]{display:none!important}\n.dshm-details{position:fixed;z-index:80;inset:0 0 0 auto;box-sizing:border-box;width:min(94vw,460px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-layer-1,#fff),var(--dsw-alias-bg-layer-1,#fff));box-shadow:-18px 0 46px rgb(15 23 42 / 18%);transform:translateX(104%);transition:transform 190ms cubic-bezier(.22,1,.36,1)}\n.dshm-details[data-open=true]{transform:translateX(0)}\n.dshm-details[data-open=false]{pointer-events:none;visibility:hidden;transition:transform 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}\n@media(max-width:899px){.dshm-details{width:100%;max-width:100%;box-shadow:none}.dshm-details [data-sidebar-right-toggle],.dshm-details [data-sidebar-right-mode]{min-width:48px;min-height:48px}}\n.dshm-scrim{position:fixed;z-index:65;inset:0;border:0;background:rgb(15 23 42 / 40%);opacity:0;pointer-events:none;transition:opacity 180ms ease-out}\n.dshm-scrim[data-open=true]{opacity:1;pointer-events:auto}\n.dshm-overlay{position:fixed;z-index:90;inset:0;pointer-events:none}.dshm-overlay>*{pointer-events:auto}\n/* Reserve navigation space instead of covering a panel's own save controls. */\n.dshm-panelNav{display:flex;align-items:center;flex-shrink:0;min-height:56px;box-sizing:border-box;padding:max(4px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 4px max(68px,env(safe-area-inset-left));border-bottom:.5px solid var(--dsw-alias-border-l4);background-color:Canvas;background-image:linear-gradient(var(--dsw-alias-bg-base,#fff),var(--dsw-alias-bg-base,#fff))}\n.dshm-panelBack{display:inline-flex;align-items:center;gap:8px;min-width:48px;min-height:48px;padding:8px 12px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,#171a21);font:inherit;font-size:14px;line-height:1.4;text-align:left;cursor:pointer}\n.dshm-panelBack:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}\n.dshm-panelBack:active{background:var(--dsw-alias-interactive-bg-active,#e9ecef)}\n.dshm-panelBack:disabled{opacity:.5;cursor:default}\n.dshm-panelBack:focus-visible{outline:2px solid var(--dsw-alias-label-primary,#171a21);outline-offset:2px}\n.dshm-panelBack svg{width:20px;height:20px;flex-shrink:0}\n.dshm-shell header{min-width:0;padding-left:52px}\n/* Keep focused editables above Safari's 16px zoom threshold while retaining\n the DSH content-font preference and larger inherited text. Stock menus\n portal into document.body, so the floor scopes from the body itself. */\nbody:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable=plaintext-only],[contenteditable=\"\"]){font-size:max(16px,1em,var(--dsh-content-font-size,1em))!important}\n.dshm-shell table{display:block;max-width:100%;overflow-x:auto}\n.dshm-shell pre{max-width:100%;overflow-x:auto}\n.dshm-shell img,.dshm-shell video,.dshm-shell canvas,.dshm-shell svg{max-width:100%}\n.dshm-shell [data-disclosure-row]{min-width:0;max-width:100%}\n.dshm-shell [data-disclosure-row]>*{min-width:0;overflow-wrap:anywhere}\n.dshm-shell [data-context-fields]>*{min-width:0}\n.dshm-shell [class*=\"_body\"]{max-width:100%;overflow-wrap:anywhere}\n@media(max-width:420px){.dshm-shell [data-context-fields]>*{display:grid;grid-template-columns:1fr!important;gap:4px}.dshm-shell [class*=\"_ioSection\"]{grid-template-columns:1fr!important}}\n@media(max-width:600px){\n.dshm-shell [data-question-key],.dshm-shell [data-plan-review-key]{box-sizing:border-box;width:100%;height:auto!important;min-width:0;flex:none!important;align-self:flex-end;padding:6px max(10px,env(safe-area-inset-left)) max(10px,env(safe-area-inset-bottom)) max(10px,env(safe-area-inset-right))!important}\n.dshm-shell [data-question-key]>section,.dshm-shell [data-plan-review-key]>section{width:100%;height:auto!important;min-height:0!important;max-width:none!important;max-height:min(68dvh,520px)!important;border-radius:16px!important}\n.dshm-shell [data-question-scroll],.dshm-shell [data-plan-review-scroll]{flex:0 1 auto!important;min-height:0!important;max-height:min(42dvh,360px)!important;overscroll-behavior:contain;scroll-padding-bottom:12px}\n.dshm-shell [data-question-key]>section>header{display:flex!important;visibility:visible!important;flex:none!important;gap:8px!important;padding:12px 8px 4px 14px!important}\n.dshm-shell [data-question-key]>section>header h2{min-width:0;overflow-wrap:anywhere;font-size:16px!important;line-height:22px!important}\n.dshm-shell [data-question-key]>section>header button{min-width:40px;min-height:40px}\n.dshm-shell [data-question-key] [role=radio],.dshm-shell [data-question-key] [role=checkbox]{min-height:48px!important;touch-action:manipulation}\n.dshm-shell [data-question-key]>section>footer{display:grid!important;grid-template-columns:auto minmax(0,1fr);align-items:center!important;flex:none!important;gap:6px 8px!important;margin-top:4px!important;padding:6px 10px 10px!important}\n.dshm-shell [data-question-key]>section>footer>:last-child{grid-column:1/-1;display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));width:100%;gap:8px!important}\n.dshm-shell [data-question-key]>section>footer>:last-child button{width:100%;min-height:44px}\n.dshm-shell [data-plan-review-key]>section>div:last-child{display:grid!important;grid-template-columns:1fr;gap:8px!important;padding:8px 12px 10px!important}\n.dshm-shell [data-plan-review-key]>section>div:last-child>div:last-child{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));width:100%;gap:8px!important}\n.dshm-shell [data-plan-review-key]>section>div:last-child>div:last-child>button:first-child{grid-column:1/-1}\n.dshm-shell [data-plan-review-key]>section>div:last-child button{width:100%;min-height:44px}\n}\n@media(min-width:900px){\n.dshm-shell{grid-template-columns:auto minmax(0,1fr)}\n.dshm-main{grid-area:1/2}\n.dshm-drawer{position:static;grid-area:1/1;box-shadow:none}\n.dshm-drawer[data-open=true]{width:340px;box-shadow:none}\n.dshm-drawer[data-open=false]{width:56px}\n}\n@media(prefers-reduced-motion:reduce){.dshm-drawer,.dshm-details,.dshm-scrim,.dshm-drawer>*,.dshm-main{transition:none!important}}\n`\n\nfunction MobileAppFrame(props: MobileRootProps & {\n readonly controller: MobileLayoutController\n readonly requestDetailsClose: () => void\n}): ReactNode {\n const state = useSyncExternalStore(props.controller.subscribe, props.controller.getSnapshot)\n const suppressKeyboardUntil = useRef(0)\n const suppressComposerUntil = useRef(0)\n const previousPanelId = useRef(state.panelInfo.activePanelId)\n const navigationIme = useRef(new Map<HTMLElement, string | null>())\n const restoreNavigationIme = (): void => {\n for (const [editor, previous] of navigationIme.current) {\n if (editor.getAttribute('inputmode') !== 'none') continue\n if (previous === null) editor.removeAttribute('inputmode')\n else editor.setAttribute('inputmode', previous)\n }\n navigationIme.current.clear()\n }\n useLayoutEffect(() => {\n const previous = previousPanelId.current\n previousPanelId.current = state.panelInfo.activePanelId\n if (previous === null || state.panelInfo.activePanelId !== null\n || !window.matchMedia(TOUCH_PRIMARY_QUERY).matches) return\n // All panel exits precede the remounted composer's passive autofocus.\n restoreNavigationIme()\n suppressComposerUntil.current = performance.now() + 500\n const active = document.activeElement\n if (active instanceof HTMLElement && active.matches(COMPOSER_EDITOR_SELECTOR)) active.blur()\n }, [state.panelInfo.activePanelId])\n const [viewportWidth, setViewportWidth] = useState(window.innerWidth)\n useEffect(() => {\n let frame: number | undefined\n const resize = (): void => {\n if (frame !== undefined) return\n frame = window.requestAnimationFrame(() => {\n frame = undefined\n setViewportWidth(window.innerWidth)\n })\n }\n window.addEventListener('resize', resize)\n return () => {\n window.removeEventListener('resize', resize)\n if (frame !== undefined) window.cancelAnimationFrame(frame)\n }\n }, [])\n const wideViewport = isWideViewportLayout(viewportWidth)\n const rightbar = resolveMobileRightbarLayout(\n viewportWidth,\n state.sidebarOpen,\n state.rightbarTrack,\n state.rightbarFullscreen,\n )\n const rightbarDocked = state.detailsOpen && rightbar.docked\n // DSH reports a fullscreen panel through the layout service; the overlay has\n // to hand over the whole viewport instead of its docked track (see the\n // `data-rightbar-fullscreen` rule). Phones derive fullscreen from their\n // width, so the flag is already set there.\n const rightbarFullscreen = state.detailsOpen && state.rightbarFullscreen\n // The panel owns the viewport on phones (no column fits) and in fullscreen on\n // a wider screen, so the conversation behind it is inert and needs no scrim.\n const rightbarFullWidth = state.detailsOpen && !rightbarDocked && (!wideViewport || rightbarFullscreen)\n const [documentLanguage, setDocumentLanguage] = useState(document.documentElement.lang)\n const browserLanguages = navigator.languages.length > 0 ? navigator.languages : [navigator.language]\n const language = resolveMobileLayoutLanguage(documentLanguage, browserLanguages)\n const messages = MOBILE_LAYOUT_MESSAGES[language]\n const scrimOpen = !rightbarFullWidth && isMobileScrimOpen(state.sidebarOpen, state.detailsOpen && !rightbarDocked, wideViewport)\n const activeSessionId = props.useSessions(session => {\n const current = session.current\n return current !== undefined && session.byId[current]?.blank === false ? current : undefined\n })\n const hasSession = activeSessionId !== undefined\n\n useEffect(() => {\n const observer = new MutationObserver(() => { setDocumentLanguage(document.documentElement.lang) })\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })\n return () => { observer.disconnect() }\n }, [])\n\n useEffect(() => {\n if (!hasSession) props.controller.closeDetails()\n }, [hasSession, props.controller])\n\n // Mirror the stock layout: project the current session title into the\n // browser tab, restoring the product title when no session is selected.\n const sessionTitle = props.useSessions(session =>\n session.current === undefined ? undefined : session.byId[session.current]?.title,\n )\n useEffect(() => {\n const productTitle = document.title\n if (sessionTitle !== undefined) document.title = `${sessionTitle} — ${productTitle}`\n return () => { document.title = productTitle }\n }, [sessionTitle])\n\n useEffect(() => {\n const elementTarget = (target: EventTarget | null): Element | null =>\n target instanceof Element ? target : null\n const suppressAutofocus = (event: FocusEvent): void => {\n const target = elementTarget(event.target)\n if (target === null) return\n const editable = target instanceof HTMLElement && (target.matches('input,textarea') || target.isContentEditable)\n if (!editable) return\n if (isComposerOwnedFocus(target)) {\n if (performance.now() < suppressComposerUntil.current) target.blur()\n return\n }\n if (performance.now() >= suppressKeyboardUntil.current) return\n target.blur()\n }\n const suppressBranchAutofocus = (event: MouseEvent): void => {\n if (!(event.target instanceof Element)) return\n const branch = event.target.closest('button[aria-label*=\"分支\"],button[aria-label*=\"Branch\"],button[aria-label*=\"branch\"],button[aria-label*=\"Ramo\"],button[aria-label*=\"ramo\"]')\n if (branch === null || branch.hasAttribute('disabled') || branch.getAttribute('aria-disabled') === 'true') return\n suppressKeyboardUntil.current = performance.now() + 700\n window.setTimeout(() => {\n const active = document.activeElement\n if (active instanceof HTMLElement && (active.matches('input,textarea') || active.isContentEditable)) active.blur()\n }, 0)\n }\n // The native composer deliberately preserves editor focus when its Add menu\n // opens, and on a phone that focus summons the soft keyboard. Withholding the\n // IME leaves the focus, the caret, and the draft exactly as stock left them;\n // any other tap restores the keyboard.\n const applyComposerImePolicy = (event: PointerEvent): void => {\n const target = elementTarget(event.target)\n // A deliberate tap in the composer ends the navigation-only suppression\n // before focus can reach the editor. The Add trigger then reclaims its\n // existing inputmode=\"none\" policy below.\n if (isComposerOwnedFocus(target)) {\n suppressComposerUntil.current = 0\n restoreNavigationIme()\n }\n const policy = resolveComposerImePolicy(\n target,\n window.matchMedia(TOUCH_PRIMARY_QUERY).matches,\n )\n if (policy === 'ignore') return\n for (const editor of document.querySelectorAll<HTMLElement>(COMPOSER_EDITOR_SELECTOR)) {\n if (policy === 'withhold') editor.setAttribute('inputmode', 'none')\n else if (editor.getAttribute('inputmode') === 'none') editor.removeAttribute('inputmode')\n }\n }\n document.addEventListener('focusin', suppressAutofocus, true)\n document.addEventListener('click', suppressBranchAutofocus, true)\n // A finger tap never fires mousedown on Android, and this has to land before\n // the trigger's own focus, so it runs on capture for pointerdown.\n document.addEventListener('pointerdown', applyComposerImePolicy, true)\n const disposeSendKeyboard = installComposerSendKeyboardPolicy(() => {\n suppressComposerUntil.current = performance.now() + 700\n })\n return () => {\n document.removeEventListener('focusin', suppressAutofocus, true)\n document.removeEventListener('click', suppressBranchAutofocus, true)\n document.removeEventListener('pointerdown', applyComposerImePolicy, true)\n disposeSendKeyboard()\n restoreNavigationIme()\n // A withheld editor outliving the surface would strand the composer without\n // a keyboard for the rest of the session.\n for (const editor of document.querySelectorAll<HTMLElement>(`${COMPOSER_CARD_SELECTOR} [inputmode=\"none\"]`)) {\n editor.removeAttribute('inputmode')\n }\n }\n }, [])\n\n // Session navigation on touch withholds upstream composer autofocus; only a\n // narrow overlay drawer closes after the row's own click handler completes.\n const closeDrawerAfterSessionAction = (event: { readonly target: EventTarget | null }): void => {\n if (!(event.target instanceof Element)) return\n if (isSessionRowNavigation(event.target) && window.matchMedia(TOUCH_PRIMARY_QUERY).matches) {\n const deadline = performance.now() + 500\n suppressComposerUntil.current = deadline\n for (const editor of document.querySelectorAll<HTMLElement>(COMPOSER_EDITOR_SELECTOR)) {\n if (!navigationIme.current.has(editor)) navigationIme.current.set(editor, editor.getAttribute('inputmode'))\n editor.setAttribute('inputmode', 'none')\n if (document.activeElement === editor) editor.blur()\n }\n window.setTimeout(() => {\n if (suppressComposerUntil.current !== deadline) return\n suppressComposerUntil.current = 0\n restoreNavigationIme()\n }, 500)\n }\n if (viewportIsWide()) return\n const row = event.target.closest<HTMLElement>('[role=\"treeitem\"][aria-selected]')\n const action = event.target.closest('button,[role=\"button\"]')\n const startsSession = action?.matches('button[class*=\"_newSession\"],button[class*=\"_brand\"]')\n || /新建会话|新会话|new session|new conversation|nuova sessione|nuova conversazione/i.test(action?.getAttribute('aria-label') ?? '')\n if (row === null && !startsSession) return\n if (row !== null && action !== null && action !== row) return\n suppressKeyboardUntil.current = performance.now() + 500\n // Let the session row finish its own click handler before unmounting the drawer.\n window.setTimeout(() => {\n props.controller.closeSidebar()\n const active = document.activeElement\n if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) active.blur()\n }, 0)\n }\n\n return createElement('div', {\n className: 'dshm-shell', lang: language,\n 'data-rightbar-docked': rightbarDocked,\n 'data-rightbar-fullscreen': rightbarFullscreen,\n style: { '--dshm-rightbar-width': `${rightbar.width}px` },\n },\n createElement('main', {\n className: 'dshm-main',\n 'data-dsh-mobile-session': activeSessionId,\n ...(rightbarFullWidth ? { inert: '', 'aria-hidden': true } : {}),\n // Legacy dsh-web community plugins use semantic pane attributes as their\n // compatibility path when the stock CSS-module column names are absent.\n 'data-pane': 'conversation',\n },\n state.panelInfo.activePanelId !== null && !wideViewport ? createElement('div', {\n className: 'dshm-panelNav',\n }, createElement('button', {\n type: 'button',\n className: 'dshm-panelBack',\n 'aria-label': messages.backToConversation,\n disabled: state.sidebarOpen,\n onClick: () => { props.controller.selectPanel(null) },\n }, createElement('svg', {\n 'aria-hidden': true, viewBox: '0 0 24 24', fill: 'none',\n }, createElement('path', {\n d: 'M15 5l-7 7 7 7',\n stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n })), createElement('span', null, messages.backToConversation))) : null,\n createElement('div', { className: 'dshm-mainContent' }, props.renderSlot('main', {}, {\n entryKey: state.panelInfo.activePanelId ?? 'conversation',\n fallback: props.renderSlot('conversation', {}),\n }))),\n createElement('button', {\n 'aria-label': messages.closePanels,\n className: 'dshm-scrim',\n // A persistent wide sidebar needs no dimming; the scrim only covers\n // the narrow overlay drawer and the details panel.\n 'data-open': scrimOpen,\n 'aria-hidden': !scrimOpen,\n onClick: () => {\n if (!scrimOpen) return\n state.detailsOpen ? props.requestDetailsClose() : props.controller.closeSidebar()\n },\n tabIndex: scrimOpen ? 0 : -1,\n type: 'button',\n }),\n createElement('aside', {\n 'aria-label': messages.workspaceNavigation,\n className: 'dshm-drawer',\n 'data-pane': 'sidebar',\n 'data-open': state.sidebarOpen,\n // The phone adapter promotes the whale toggle to a fixed high layer.\n // Remove that entire navigation surface while the right modal owns focus.\n 'data-right-modal': state.detailsOpen && !rightbarDocked,\n ...(state.detailsOpen && !rightbarDocked ? { inert: '', 'aria-hidden': true } : {}),\n // Community rows collapse their label when an ancestor carries this marker.\n ...(state.sidebarOpen ? {} : { 'data-sidebar-collapsed': '' }),\n onClickCapture: closeDrawerAfterSessionAction,\n }, props.renderSlot('sidebar', {\n collapsed: !state.sidebarOpen,\n width: state.sidebarOpen ? 340 : 56,\n })),\n createElement('aside', {\n 'aria-hidden': !state.detailsOpen,\n className: 'dshm-details',\n 'data-open': state.detailsOpen,\n ...(state.detailsOpen ? {} : { inert: '' }),\n }, [\n // DSH 0.1.3 and earlier bind details to a Session. Newer DSH owns the\n // Session decision inside its root-scoped rightbar occupant, so that\n // occupant must stay mounted even while no Session is selected.\n hasSession ? props.renderSlot('details', {}) : undefined,\n props.renderSlot('rightbar', {\n width: rightbar.width,\n viewportWidth,\n // The official frame reports whether a persistent column still fits\n // (normal.rightbar > 0); this layout renders the right panel in the\n // details overlay, which needs no column space. Reporting false would\n // make RightbarSeat force the surface closed immediately after opening\n // it (ui-sidebar-right: `shown && !fullscreen && !canShow` ->\n // actions.setExpanded(false)), so the panel could never stay open on a\n // phone-width viewport.\n canShow: true,\n }),\n ]),\n createElement('div', { className: 'dshm-overlay', 'data-shell-overlay': true }, props.renderSlot('shell.overlay', {})),\n )\n}\n\n// One controller per document: re-applying the layout (reconnect, tab\n// refocus) must not reset the user's explicit sidebar toggle.\nlet sharedController: MobileLayoutController | undefined\n\n/** Replace the desktop layout module on the authenticated mobile surface. */\nexport function apply(ctx: MobileClientContext): void {\n sharedController ??= new MobileLayoutController()\n const controller = sharedController\n ctx.effect(() => {\n const style = document.createElement('style')\n style.dataset.plugin = 'dsh-mobile-layout'\n style.textContent = MOBILE_LAYOUT_STYLES\n document.head.append(style)\n const disposeMainPanelRegistry = controller.bindMainPanelRegistry(id =>\n ctx.slots.entries('main').some(entry => entry.options.key === id))\n const disposeService = ctx.reflect.provide('layout', controller)\n // The official layout owns the root `panelInfo` hook; replacing the layout\n // module means replacing that owner. Without it every registration that\n // declares the standard `usePanelInfo` prop fails assembly\n // (\"strict standard hook 'panelInfo' has no source\") and renders as a dead\n // cell — the workspace sidebar's session list lives exactly there.\n const disposePanelInfo = typeof ctx.slots.provideRoot === 'function'\n ? ctx.slots.provideRoot({ hooks: { panelInfo: controller.panelInfo } })\n : () => {}\n const requestDetailsClose = (): void => {\n closeDetailsFromScrim(ctx.get('sidebarRight'), () => { controller.closeDetails() })\n }\n const disposeRoot = ctx.slots.register<MobileRootProps>({\n name: 'root',\n children: {\n sidebar: { kind: 'single', scope: 'root' },\n conversation: { kind: 'single', scope: 'session-maybe' },\n // The conversation panel moved into this keyed slot in DSH 0.1.5;\n // without the declaration the center column stays blank.\n main: { kind: 'keyed', scope: 'root' },\n rightbar: { kind: 'single', scope: 'root' },\n details: { kind: 'single', scope: 'session' },\n 'shell.overlay': { kind: 'list', scope: 'root' },\n },\n }, props => createElement(MobileAppFrame, {\n ...props,\n controller,\n // sidebarRight is registered after the layout it depends on, so resolve\n // the optional service at gesture time rather than caching it at boot.\n requestDetailsClose,\n }))\n const onNativeBack = (event: Event): void => {\n if (!event.cancelable || event.defaultPrevented) return\n const state = controller.getSnapshot()\n if (state.detailsOpen) {\n event.preventDefault()\n requestDetailsClose()\n } else if (!viewportIsWide() && state.sidebarOpen) {\n event.preventDefault()\n controller.closeSidebar()\n } else if (state.panelInfo.activePanelId !== null) {\n event.preventDefault()\n controller.selectPanel(null)\n }\n }\n window.addEventListener('dsh-mobile:native-back', onNativeBack)\n const retainMainPanels = (): void => {\n controller.retainMainPanels(ctx.slots.entries('main').flatMap(entry =>\n entry.options.key === undefined ? [] : [entry.options.key]))\n }\n const disposePanels = ctx.slots.subscribe('main', retainMainPanels)\n retainMainPanels()\n return () => {\n window.removeEventListener('dsh-mobile:native-back', onNativeBack)\n controller.dispose()\n disposePanels()\n disposeRoot()\n disposePanelInfo()\n void disposeService()\n disposeMainPanelRegistry()\n style.remove()\n }\n }, 'dsh-mobile: dedicated root layout')\n\n ctx.effect(() => {\n const presenter = new ThemePresenter()\n presenter.apply(ctx.theme.getTheme())\n const off = ctx.on('theme/change', snapshot => { presenter.apply(snapshot) })\n return () => { off(); presenter.dispose() }\n }, 'dsh-mobile: theme presenter')\n}\n\n/** Preserve the official layout module's dependency ordering. */\nexport const inject: readonly string[] = ['slots', 'theme']\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guangqi Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zexadev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ The Rust transport sources in native/iroh-host, native/iroh-core and
2
+ native/iroh-android-transport were adapted from saya-ch/dsh-mobile-iroh,
3
+ copyright 2026 Guangqi Zhang, under the MIT License. The complete license is
4
+ preserved in LICENSE.dsh-mobile-iroh.
5
+
6
+ The host baseline also adapts the MIT-licensed dsh-tether reference project
7
+ by zexadev. Its original notice and license are preserved in
8
+ LICENSE.dsh-tether. See https://github.com/zexadev/dsh-tether.
9
+
10
+ The surrounding DSH Mobile plugin, Android shell and development fixtures
11
+ remain under this repository's Apache License, Version 2.0. Importing the
12
+ Rust transport does not relicense those portions. This experimental
13
+ integration does not imply upstream endorsement or completed device acceptance.
@@ -0,0 +1,66 @@
1
+ # iroh host byte carrier
2
+
3
+ This crate owns one authenticated iroh endpoint and its paired peer registry. It forwards bounded concurrent streams only to one explicitly configured loopback TCP socket. The forwarded bytes are not parsed or rewritten: a TLS gateway retains its own certificate, Host, Origin, cookies, CSRF and WebSocket authorization checks. iroh pairing is transport authorization, not a replacement for DSH Mobile device credentials.
4
+
5
+ ## Commands
6
+
7
+ ```powershell
8
+ cargo build --locked -p dsh-mobile-iroh-host
9
+ ./target/debug/dsh-mobile-iroh-host host --data-dir <private-directory> --pair --proxy-target 127.0.0.1:3443
10
+ ```
11
+
12
+ The parent owns stdin and stdout. Closing stdin or sending `shutdown` closes the endpoint, active peer connections and their proxy tasks, then emits `stopped`. One OS file lock prevents simultaneous hosts from using the same data directory. Corrupt or unreadable identity/paired-device files fail startup rather than generating a new identity or opening a new pairing window.
13
+
14
+ Windows opens the verified private lock file without delete sharing, so its active pathname cannot be unlinked or renamed to bypass the lock. On Unix, the profile directory must remain exclusively application-owned and its `host.lock` pathname must not be manually removed while a host runs: advisory file locking does not prevent a process with the same UID from unlinking a locked inode. A compromised current user is outside the local credential isolation model.
15
+
16
+ On Windows, the default profile is `%USERPROFILE%\.dsh-mobile-iroh`. New private directories and secret/temporary files receive a protected DACL at creation, granting only the current user, SYSTEM and Administrators. Reads verify the existing owner and DACL before reading bytes through the same non-reparse handle. Every ancestor is checked for reparse points and untrusted replacement rights, with non-delete-shared handles held during the operation. An existing broadly accessible directory/file is rejected, not silently repaired; select a new private profile directory instead. Shared development folders and some sandbox-modified Temp/AppData directories therefore cannot be used as credential stores. Existing broad parent directories are never reconfigured by this implementation.
17
+
18
+ Windows acceptance includes real DACL inspection, current-token access allowed, an Everyone-restricted token rejected by `AccessCheck`, existing world-readable files rejected without replacement, a real owned junction rejected, and concurrent/exclusive temporary-file checks. This is same-machine restricted-token evidence, not an actual second-user login or a defense against a compromised current user, administrator or SYSTEM account.
19
+
20
+ On non-Android Unix, a non-sticky ancestor may not be group-writable even when its group is GID 0. Group membership is not equivalent to root UID. Android's explicitly trusted system/app ancestor groups are separate from this rule; the application's private leaf still forbids all group/other access.
21
+
22
+ On POSIX systems, existing directory/file ownership and modes are verified without `chmod`: broad legacy files such as a `0644` identity are rejected before reading and remain unchanged. New profile directories and files request `0700`/`0600`; safe stricter owner permissions remain valid. Parent traversal uses directory descriptors with `O_NOFOLLOW`, `openat` and `fstat`; reads, lock creation and atomic `renameat`/`unlinkat` keep the verified parent descriptor. Linux/Android use `O_PATH` for execute-only ancestors, then reopen the verified private leaf for directory sync. macOS uses `O_SEARCH`. Fixed system-owned `/var`, `/tmp`, `/home` and Android `/data/data`/`/data/user/0` aliases are resolved to an actual root that is then independently verified; arbitrary user symlinks are rejected. A root-owned sticky temporary ancestor is allowed, but the application profile below it must remain current-user-private. Android system UID/GID 1000 is trusted only as an operating-system ancestor, never as a substitute owner for the App's private leaf.
23
+
24
+ `--local-only` on both `host` and `phone-sim` disables public relay and discovery and binds iroh to loopback. Copy each `ready.direct_addresses` value to a repeated `phone-sim --peer-address <socket>` argument, and also supply `--peer <endpoint_id>`. Direct addresses never replace the authenticated endpoint identity. `--proxy-listen 127.0.0.1:0` selects an available local port; the actual origin is returned in `proxy-ready`. Local-only checks do not establish public NAT or relay behavior.
25
+
26
+ ## Parent stdio protocol, version 1
27
+
28
+ Every newline-terminated UTF-8 command includes `"version":1`. Unknown/duplicate fields, explicit nulls, unsupported commands and lines over 16 KiB are rejected. Invalid commands stop the sidecar rather than leaving a parent with uncertain authorization state. Commands contain no gateway/DSH credential injection operation.
29
+
30
+ | Command | Additional fields |
31
+ | --- | --- |
32
+ | `pairing-begin` | None |
33
+ | `device-list` | None |
34
+ | `device-forget` | `id`: canonical authenticated EndpointId |
35
+ | `shutdown` | None |
36
+
37
+ Every stdout event includes `"version":1` and `type`.
38
+
39
+ | Event | Additional fields |
40
+ | --- | --- |
41
+ | `ready` | `endpoint_id`, `direct_addresses` (socket strings), `local_only` |
42
+ | `pairing` | `code`, `expires_in_sec` |
43
+ | `pairing-closed` | `reason` |
44
+ | `pairing-done` | `peer`, `name` |
45
+ | `peer-connected` | `peer`, `name` |
46
+ | `peer-path` | `peer`, `kind` (`direct` or `relay`), `remote` |
47
+ | `peer-disconnected` | `peer` |
48
+ | `proxy-opened` | `peer` (first accepted proxy attempt per connection) |
49
+ | `devices` | `devices`: `{id,name,paired_at,online}[]` |
50
+ | `error` | `code`, `message` (sanitized, not the original command) |
51
+ | `stopped` | None |
52
+ | `proxy-ready` | `origin` (phone-sim only, after handshake ACK and successful local bind) |
53
+
54
+ The pairing code is an intentional private control response for the owning parent; do not mirror it into telemetry or human-readable logs. Device and path events contain identifiers/network addresses, not DSH credentials or model content. Proxy payloads are never logged.
55
+
56
+ ## Peer protocol
57
+
58
+ ALPN is `dsh-mobile-iroh/0`. Every first control or proxy-stream line requires `"version":1`; the decoder is shared by host, Android transport and phone-sim. A new peer sends `pair` with a six-ASCII-digit `code` and a trimmed 1–64 character `name`. Only a successful private-state write permits `pair-ok`. A paired peer sends `hello` with `name` and must receive `hello-ok` before exposing its local proxy. Unknown fields and extra post-handshake control messages are rejected.
59
+
60
+ The control stream remains owned for the entire connection. EOF, protocol error, replacement or revocation closes the connection and all proxy tasks. A later connection generation cannot be removed by an older handler's teardown. An already paired endpoint can explicitly pair again with a fresh valid pairing window, which permits recovery when gateway authorization failed after transport pairing; this updates the existing row, does not duplicate it and never bypasses the new code. `device-forget` persists revocation before changing in-memory authorization or closing the targeted peer; a persistence failure stops the host rather than reporting success. Other peers are not revoked.
61
+
62
+ Each later bidirectional stream begins with `{"version":1,"type":"proxy"}` plus a newline, then carries unmodified TCP bytes. QUIC's authenticated transport protects network replay; this carrier does not accept standalone approvals or decisions. The host bounds concurrent connections, per-peer/global proxy streams, handshake time, proxy discriminator time and loopback connect time. Streams that exceed capacity are reset rather than queued indefinitely. Long-running authenticated payload streams remain open until the gateway, peer or owner closes them.
63
+
64
+ ## Verification
65
+
66
+ `cargo test --locked -p dsh-mobile-iroh-core -p dsh-mobile-iroh-host` includes real offline QUIC endpoints, handshake rejection, durable pairing and Hello ACK, same-peer replacement, raw HTTP/cookies/WebSocket/binary forwarding, revocation, control EOF and persistence failure. These checks do not establish Android WebView integration, production DSH authorization or public-network support. Process-level smoke is owned by the repository's JavaScript harness.