@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.1

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 (126) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +56 -26
  3. package/README.zh.md +20 -15
  4. package/assets/mobile-icon-192.png +0 -0
  5. package/assets/mobile-icon-512.png +0 -0
  6. package/assets/mobile-service-worker.js +64 -0
  7. package/lib/client.js +272 -47
  8. package/lib/client.js.map +1 -1
  9. package/lib/index.js +316 -60
  10. package/lib/mobile.js +487 -56
  11. package/lib/mobile.js.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.d.ts +28 -0
  13. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PluginSettingsCard.js +147 -6
  15. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  16. package/lib/types/client/RemoteEntry.js +13 -2
  17. package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +1 -1
  20. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  21. package/lib/types/client/UpdateEntry.js +36 -3
  22. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  23. package/lib/types/client/UpdatePanel.js +2 -2
  24. package/lib/types/client/deep-link.js +1 -1
  25. package/lib/types/client/locales.d.ts +4 -0
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +4 -0
  28. package/lib/types/gate.d.ts +9 -0
  29. package/lib/types/gate.d.ts.map +1 -1
  30. package/lib/types/gate.js +17 -4
  31. package/lib/types/index.d.ts +25 -0
  32. package/lib/types/index.d.ts.map +1 -1
  33. package/lib/types/index.js +30 -13
  34. package/lib/types/mobile/PairRequiredView.d.ts +7 -0
  35. package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
  36. package/lib/types/mobile/PairRequiredView.js +28 -0
  37. package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
  38. package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
  39. package/lib/types/mobile/PairRequiredView.test.js +24 -0
  40. package/lib/types/mobile/api.d.ts +10 -0
  41. package/lib/types/mobile/api.d.ts.map +1 -1
  42. package/lib/types/mobile/api.js +4 -0
  43. package/lib/types/mobile/index.js +14 -1
  44. package/lib/types/mobile/messages.d.ts +24 -0
  45. package/lib/types/mobile/messages.d.ts.map +1 -1
  46. package/lib/types/mobile/messages.js +60 -23
  47. package/lib/types/mobile/messages.test.js +49 -1
  48. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  49. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  50. package/lib/types/mobile/mobile-styles.js +86 -0
  51. package/lib/types/mobile/pairing.d.ts +31 -0
  52. package/lib/types/mobile/pairing.d.ts.map +1 -0
  53. package/lib/types/mobile/pairing.js +62 -0
  54. package/lib/types/mobile/views/App.d.ts +12 -5
  55. package/lib/types/mobile/views/App.d.ts.map +1 -1
  56. package/lib/types/mobile/views/App.js +34 -7
  57. package/lib/types/mobile/views/App.test.d.ts +2 -0
  58. package/lib/types/mobile/views/App.test.d.ts.map +1 -0
  59. package/lib/types/mobile/views/App.test.js +52 -0
  60. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  61. package/lib/types/mobile/views/ChatView.js +34 -11
  62. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  63. package/lib/types/mobile/views/SessionListView.js +30 -4
  64. package/lib/types/mobile/views/SessionListView.test.js +58 -1
  65. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  66. package/lib/types/mobile/views/WorkspaceView.js +4 -2
  67. package/lib/types/mobile-api.d.ts.map +1 -1
  68. package/lib/types/mobile-api.js +10 -1
  69. package/lib/types/mobile-pwa.d.ts +6 -0
  70. package/lib/types/mobile-pwa.d.ts.map +1 -0
  71. package/lib/types/mobile-pwa.js +17 -0
  72. package/lib/types/mobile-routes.d.ts +5 -8
  73. package/lib/types/mobile-routes.d.ts.map +1 -1
  74. package/lib/types/mobile-routes.js +115 -30
  75. package/lib/types/pairing-access.d.ts +28 -0
  76. package/lib/types/pairing-access.d.ts.map +1 -0
  77. package/lib/types/pairing-access.js +26 -0
  78. package/lib/types/pairing.d.ts +27 -0
  79. package/lib/types/pairing.d.ts.map +1 -1
  80. package/lib/types/pairing.js +68 -0
  81. package/lib/types/pairing.persist.test.d.ts +2 -0
  82. package/lib/types/pairing.persist.test.d.ts.map +1 -0
  83. package/lib/types/pairing.persist.test.js +128 -0
  84. package/lib/types/routes.d.ts.map +1 -1
  85. package/lib/types/routes.js +23 -3
  86. package/lib/types/tunnel.d.ts +1 -0
  87. package/lib/types/tunnel.d.ts.map +1 -1
  88. package/lib/types/tunnel.js +7 -2
  89. package/lib/types/update.d.ts +4 -4
  90. package/lib/types/update.d.ts.map +1 -1
  91. package/lib/types/update.js +35 -12
  92. package/package.json +12 -12
  93. package/src/client/PluginSettingsCard.tsx +229 -18
  94. package/src/client/RemoteEntry.tsx +12 -2
  95. package/src/client/RemoteSettingsCard.tsx +3 -2
  96. package/src/client/UpdateEntry.tsx +36 -4
  97. package/src/client/UpdatePanel.tsx +2 -0
  98. package/src/client/deep-link.ts +1 -1
  99. package/src/client/locales.ts +4 -0
  100. package/src/client/remote.module.css +26 -0
  101. package/src/client/settings-card.module.css +90 -1
  102. package/src/gate.ts +17 -3
  103. package/src/index.ts +46 -15
  104. package/src/mobile/PairRequiredView.test.tsx +28 -0
  105. package/src/mobile/PairRequiredView.tsx +58 -0
  106. package/src/mobile/api.ts +14 -1
  107. package/src/mobile/index.tsx +15 -1
  108. package/src/mobile/messages.test.ts +52 -1
  109. package/src/mobile/messages.ts +65 -22
  110. package/src/mobile/mobile-styles.ts +86 -0
  111. package/src/mobile/pairing.ts +77 -0
  112. package/src/mobile/views/App.test.tsx +62 -0
  113. package/src/mobile/views/App.tsx +53 -6
  114. package/src/mobile/views/ChatView.tsx +33 -10
  115. package/src/mobile/views/SessionListView.test.tsx +69 -1
  116. package/src/mobile/views/SessionListView.tsx +58 -5
  117. package/src/mobile/views/WorkspaceView.tsx +4 -2
  118. package/src/mobile-api.ts +9 -1
  119. package/src/mobile-pwa.ts +19 -0
  120. package/src/mobile-routes.ts +123 -30
  121. package/src/pairing-access.ts +36 -0
  122. package/src/pairing.persist.test.ts +145 -0
  123. package/src/pairing.ts +70 -1
  124. package/src/routes.ts +23 -3
  125. package/src/tunnel.ts +7 -2
  126. package/src/update.ts +42 -14
@@ -2,5 +2,5 @@
2
2
  * Mobile surface stylesheet, shipped as a string and injected at boot (the
3
3
  * standalone page has no CSS-module pipeline).
4
4
  */
5
- export declare const mobileCss = "/* Mobile surface chrome: standalone stylesheet (no main-UI design tokens \u2014\n this page boots without the shell). Light-first palette; the dark palette\n applies only when the header toggle sets [data-theme='dark']. */\n\n:root {\n --m-bg: #f3f5f9;\n --m-bg-raised: #ffffff;\n --m-bg-input: #ffffff;\n --m-border: #dfe4ec;\n --m-text: #1c2333;\n --m-text-secondary: #4d5768;\n --m-text-tertiary: #8b95a7;\n --m-accent: #2f6fed;\n --m-accent-soft: rgba(47, 111, 237, 0.12);\n --m-danger: #d64545;\n --m-success: #1e9e6a;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);\n --m-shadow-raise: 0 6px 20px rgba(16, 24, 40, 0.1);\n --m-backdrop: rgba(15, 23, 42, 0.4);\n}\n\n:root[data-theme='dark'] {\n --m-bg: #111418;\n --m-bg-raised: #1a1f26;\n --m-bg-input: #20262e;\n --m-border: #2b333d;\n --m-text: #e8eaed;\n --m-text-secondary: #9aa3ad;\n --m-text-tertiary: #6b747f;\n --m-accent: #4f8cff;\n --m-accent-soft: rgba(79, 140, 255, 0.14);\n --m-danger: #e5534b;\n --m-success: #3fb68b;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n --m-shadow-raise: 0 6px 20px rgba(0, 0, 0, 0.4);\n --m-backdrop: rgba(0, 0, 0, 0.55);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n height: 100%;\n background: var(--m-bg);\n color: var(--m-text);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC', 'Segoe UI', sans-serif;\n font-size: 15px;\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n}\n\n/* The app fills exactly one viewport: the page itself never scrolls, each\n view owns its scroll region (chat history, session lists) and the chat\n composer stays pinned to the bottom of the screen. */\n#root {\n height: 100dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.mobile {\n min-height: 0;\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* \u2500\u2500 header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-header {\n position: sticky;\n top: 0;\n z-index: 10;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: calc(env(safe-area-inset-top, 0px) + 12px) 16px 10px;\n background: color-mix(in srgb, var(--m-bg) 88%, transparent);\n backdrop-filter: blur(10px);\n border-bottom: 1px solid var(--m-border);\n}\n\n.mobile-title {\n margin: 0;\n font-size: 17px;\n font-weight: 600;\n}\n\n.mobile-titleInline {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mobile-back {\n flex: none;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text);\n font-size: 22px;\n line-height: 1;\n cursor: pointer;\n transition: background-color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-back:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-back:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle {\n flex: none;\n margin-left: auto;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text-secondary);\n cursor: pointer;\n transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-theme-toggle:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-theme-toggle:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle svg {\n width: 18px;\n height: 18px;\n}\n\n/* \u2500\u2500 lists \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n list-style: none;\n margin: 0;\n padding: 8px 0 calc(env(safe-area-inset-bottom, 0px) + 16px);\n}\n\n.mobile-row {\n display: flex;\n align-items: center;\n gap: 10px;\n width: calc(100% - 24px);\n margin: 0 12px 8px;\n padding: 13px 14px;\n border: 1px solid var(--m-border);\n border-radius: 14px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.mobile-row:active {\n background: color-mix(in srgb, var(--m-text) 5%, var(--m-bg-raised));\n}\n\n.mobile-rowMain {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.mobile-rowTitle {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 15px;\n}\n\n.mobile-rowMeta {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.mobile-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 18px;\n}\n\n.mobile-live {\n margin-left: 6px;\n color: var(--m-success);\n font-size: 10px;\n vertical-align: middle;\n}\n\n/* \u2500\u2500 empty / error states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px;\n}\n\n.mobile-muted {\n margin: 0;\n color: var(--m-text-tertiary);\n}\n\n.mobile-error {\n margin: 0;\n color: var(--m-danger);\n}\n\n.mobile-pad {\n padding: 8px 16px;\n}\n\n/* \u2500\u2500 buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-button {\n height: 38px;\n padding: 0 18px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.mobile-button:disabled {\n opacity: 0.5;\n}\n\n.mobile-block {\n display: block;\n width: calc(100% - 32px);\n margin: 4px 16px;\n}\n\n/* Primary action (create session): accent fill, readable on both themes. */\n.mobile-new {\n display: block;\n width: 100%;\n height: 44px;\n border: none;\n border-radius: 12px;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(47, 111, 237, 0.3);\n}\n\n.mobile-new:active {\n opacity: 0.88;\n}\n\n.mobile-new:disabled {\n opacity: 0.6;\n}\n\n.mobile-hint {\n display: block;\n margin-top: 2px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 chat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.chat-scroll {\n flex: 1;\n overflow-y: auto;\n padding: 12px 14px calc(env(safe-area-inset-bottom, 0px) + 12px);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-msg {\n max-width: 88%;\n padding: 10px 12px;\n border-radius: var(--m-radius);\n font-size: 14.5px;\n line-height: 1.55;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.chat-msg-user {\n align-self: flex-end;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n border-bottom-right-radius: 4px;\n box-shadow: 0 1px 3px rgba(47, 111, 237, 0.28);\n}\n\n.chat-msg-assistant {\n align-self: flex-start;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom-left-radius: 4px;\n box-shadow: var(--m-shadow);\n}\n\n.chat-msg-pending .chat-msg-text::after {\n content: '\u258D';\n animation: chat-blink 1s steps(2) infinite;\n}\n\n@keyframes chat-blink {\n 50% {\n opacity: 0;\n }\n}\n\n.chat-msg-failed {\n border-color: var(--m-danger);\n}\n\n.chat-msg-failtag {\n display: inline-block;\n margin-top: 6px;\n padding: 1px 8px;\n border: 1px solid var(--m-danger);\n border-radius: 999px;\n color: var(--m-danger);\n font-size: 11px;\n}\n\n.chat-msg-time {\n display: block;\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 10.5px;\n line-height: 1.2;\n}\n\n.chat-msg-toggle {\n display: block;\n margin-top: 8px;\n padding: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-accent);\n font-size: 13px;\n cursor: pointer;\n}\n\n/* Assistant markdown content (GFM subset; renderer in markdown.ts). */\n.chat-md {\n white-space: normal;\n}\n.chat-md-collapsed {\n max-height: 45vh;\n overflow: hidden;\n position: relative;\n}\n.chat-md-collapsed::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 28px;\n background: linear-gradient(transparent, var(--m-bg-raised));\n pointer-events: none;\n}\n.chat-md-body p { margin: 0 0 8px; }\n.chat-md-body p:last-child { margin-bottom: 0; }\n.chat-md-body h1, .chat-md-body h2, .chat-md-body h3,\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 {\n margin: 12px 0 6px;\n font-weight: 650;\n line-height: 1.3;\n}\n.chat-md-body h1 { font-size: 1.35em; }\n.chat-md-body h2 { font-size: 1.25em; }\n.chat-md-body h3 { font-size: 1.15em; }\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 { font-size: 1.05em; }\n.chat-md-body code {\n font-family: var(--m-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: 0.9em;\n background: var(--m-accent-soft);\n padding: 1px 5px;\n border-radius: 4px;\n}\n.chat-md-body pre {\n margin: 8px 0;\n padding: 10px;\n background: #101726;\n color: #dbe4f5;\n border-radius: 8px;\n overflow-x: auto;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.chat-md-body pre code {\n background: transparent;\n padding: 0;\n color: inherit;\n}\n.chat-md-body ul, .chat-md-body ol { margin: 4px 0 8px; padding-left: 22px; }\n.chat-md-body li { margin: 2px 0; }\n.chat-md-body blockquote {\n margin: 8px 0;\n padding: 4px 10px;\n border-left: 3px solid var(--m-accent);\n background: var(--m-accent-soft);\n border-radius: 0 6px 6px 0;\n color: var(--m-text-secondary);\n}\n.chat-md-body table {\n margin: 8px 0;\n border-collapse: collapse;\n display: block;\n overflow-x: auto;\n font-size: 13px;\n}\n.chat-md-body th, .chat-md-body td {\n border: 1px solid var(--m-border);\n padding: 4px 8px;\n}\n.chat-md-body th { background: var(--m-accent-soft); }\n.chat-md-body a { color: var(--m-accent); }\n.chat-md-body hr { border: none; border-top: 1px solid var(--m-border); margin: 10px 0; }\n.chat-md-body img { max-width: 100%; border-radius: 8px; }\n\n/* \u2500\u2500 message disclosures (reasoning / tools) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-disclosure {\n margin-bottom: 8px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: color-mix(in srgb, var(--m-bg) 55%, transparent);\n overflow: hidden;\n}\n\n.chat-disclosure-head {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 7px 10px;\n border: none;\n background: transparent;\n color: var(--m-text);\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.chat-disclosure-caret {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n line-height: 1;\n transition: transform 0.15s ease;\n}\n\n.chat-disclosure-open .chat-disclosure-caret {\n transform: rotate(90deg);\n}\n\n.chat-disclosure-label {\n flex: none;\n color: var(--m-text-secondary);\n}\n\n.chat-reasoning[data-pending] .chat-disclosure-label {\n color: var(--m-accent);\n}\n\n.chat-disclosure-summary {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-disclosure-count {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-disclosure-body {\n padding: 8px 10px 10px;\n border-top: 1px dashed var(--m-border);\n color: var(--m-text-secondary);\n font-size: 12.5px;\n line-height: 1.6;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n max-height: 40vh;\n overflow-y: auto;\n}\n\n.chat-tools-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.chat-tool-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.chat-tool-name {\n color: var(--m-text);\n font-size: 12.5px;\n font-weight: 600;\n}\n\n.chat-tool-args {\n margin: 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--m-bg-input);\n color: var(--m-text-secondary);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n/* \u2500\u2500 composer toolbar (model / permission chips) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-tools {\n display: flex;\n gap: 8px;\n padding: 6px 12px 0;\n}\n\n.chat-chip {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n font-size: 12px;\n cursor: pointer;\n}\n\n.chat-chip:active {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-chip-label {\n flex: none;\n color: var(--m-text-tertiary);\n}\n\n.chat-chip-value {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.chat-chip-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n}\n\n/* \u2500\u2500 bottom sheets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-backdrop {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n background: var(--m-backdrop);\n animation: sheet-fade 0.18s ease;\n}\n\n@keyframes sheet-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.sheet {\n max-height: 74dvh;\n display: flex;\n flex-direction: column;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom: none;\n border-radius: 16px 16px 0 0;\n box-shadow: var(--m-shadow-raise);\n padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);\n animation: sheet-up 0.22s ease;\n}\n\n@keyframes sheet-up {\n from {\n transform: translateY(40px);\n opacity: 0.6;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n.sheet-handle {\n flex: none;\n align-self: center;\n width: 36px;\n height: 4px;\n margin: 8px 0 4px;\n border-radius: 999px;\n background: var(--m-border);\n}\n\n.sheet-title {\n flex: none;\n padding: 6px 16px 10px;\n font-size: 15px;\n font-weight: 600;\n}\n\n.sheet-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 12px 8px;\n}\n\n.sheet-status {\n padding: 18px 8px;\n color: var(--m-text-tertiary);\n font-size: 13px;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n}\n\n.sheet-status-error {\n color: var(--m-danger);\n}\n\n.sheet-hint {\n margin: 0;\n color: var(--m-text-secondary);\n font-size: 12px;\n line-height: 1.5;\n max-width: 32em;\n}\n\n.sheet-error {\n margin: 0 0 8px;\n color: var(--m-danger);\n font-size: 12.5px;\n}\n\n.sheet-section {\n margin-bottom: 14px;\n}\n\n.sheet-section-title {\n padding: 4px 8px 6px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.sheet-option {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 10px 12px;\n border: 1px solid transparent;\n border-radius: 10px;\n background: transparent;\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.sheet-option:active {\n background: var(--m-bg-input);\n}\n\n.sheet-option-selected {\n border-color: var(--m-accent);\n background: color-mix(in srgb, var(--m-accent) 12%, transparent);\n}\n\n.sheet-option:disabled {\n opacity: 0.5;\n}\n\n.sheet-option-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-option-title {\n font-size: 14px;\n}\n\n.sheet-option-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-option-check {\n flex: none;\n color: var(--m-accent);\n font-size: 15px;\n font-weight: 700;\n}\n\n.sheet-confirm-desc {\n margin: 0 4px 14px;\n color: var(--m-text-secondary);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.sheet-confirm-actions {\n display: flex;\n gap: 10px;\n padding: 4px;\n}\n\n.sheet-confirm-actions .mobile-button {\n flex: 1;\n}\n\n.sheet-confirm-danger {\n flex: 1;\n height: 38px;\n border: none;\n border-radius: 10px;\n background: var(--m-danger);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.sheet-confirm-danger:disabled {\n opacity: 0.5;\n}\n\n.chat-meta {\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-typing {\n color: var(--m-text-tertiary);\n font-size: 13px;\n padding: 4px 2px;\n}\n\n.chat-inputbar {\n display: flex;\n gap: 8px;\n padding: 10px 12px calc(env(safe-area-inset-bottom, 0px) + 10px);\n border-top: 1px solid var(--m-border);\n background: var(--m-bg);\n}\n\n.chat-input {\n flex: 1;\n min-width: 0;\n min-height: 40px;\n max-height: 120px;\n /* Grow with the content (still bounded by min/max-height); browsers\n without field-sizing support ignore this line and keep the old size. */\n field-sizing: content;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n font-size: 14.5px;\n resize: none;\n outline: none;\n}\n\n.chat-input:focus {\n border-color: var(--m-accent);\n}\n\n.chat-send {\n flex: none;\n align-self: flex-end;\n height: 40px;\n padding: 0 16px;\n border: none;\n border-radius: 10px;\n background: var(--m-accent);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.chat-send:disabled {\n opacity: 0.5;\n}\n\n.chat-load-older {\n align-self: center;\n margin: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-text-secondary);\n font-size: 13px;\n cursor: pointer;\n padding: 6px 10px;\n}\n\n/* Touch surfaces have no hover, so keyboard focus must be fully visible.\n One shared ring keeps every interactive control consistent and readable\n on both palettes. */\n.mobile-row:focus-visible,\n.mobile-button:focus-visible,\n.mobile-new:focus-visible,\n.chat-send:focus-visible,\n.chat-chip:focus-visible,\n.chat-msg-toggle:focus-visible,\n.chat-load-older:focus-visible,\n.chat-disclosure-head:focus-visible,\n.sheet-option:focus-visible,\n.sheet-confirm-danger:focus-visible,\n.sheet-toggle-switch:focus-visible,\n.chat-input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n/* \u2500\u2500 chat context usage chip (chat-tools row) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-context {\n flex: none;\n align-self: center;\n padding: 2px 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: color-mix(in srgb, var(--m-text-tertiary) 10%, transparent);\n color: var(--m-text-tertiary);\n font-size: 11px;\n line-height: 1.6;\n white-space: nowrap;\n}\n\n.chat-context-warn {\n border-color: var(--m-danger);\n background: color-mix(in srgb, var(--m-danger) 14%, transparent);\n color: var(--m-danger);\n}\n\n/* \u2500\u2500 display sheet toggle rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-toggle-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 8px;\n border-bottom: 1px solid var(--m-border);\n}\n\n.sheet-toggle-row:last-child {\n border-bottom: none;\n}\n\n.sheet-toggle-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-toggle-title {\n font-size: 14px;\n}\n\n.sheet-toggle-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-toggle-switch {\n position: relative;\n flex: none;\n width: 44px;\n height: 26px;\n border: none;\n border-radius: 999px;\n background: var(--m-border);\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.sheet-toggle-switch-on {\n background: var(--m-accent);\n}\n\n.sheet-toggle-switch-knob {\n position: absolute;\n top: 3px;\n left: 3px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 2px rgba(16, 24, 40, 0.2);\n transition: transform 0.15s ease;\n}\n\n.sheet-toggle-switch-on .sheet-toggle-switch-knob {\n transform: translateX(18px);\n}\n\n/* Accessibility: collapse every interaction/enter animation for\n reduced-motion users; surface layout itself stays static. */\n@media (prefers-reduced-motion: reduce) {\n .mobile-theme-toggle,\n .mobile-back,\n .mobile-row,\n .mobile-button,\n .mobile-new,\n .chat-send,\n .chat-chip,\n .chat-msg-toggle,\n .chat-load-older,\n .chat-disclosure-head,\n .chat-disclosure-caret,\n .chat-msg-pending .chat-msg-text::after,\n .sheet-backdrop,\n .sheet,\n .sheet-option,\n .sheet-confirm-danger,\n .sheet-toggle-switch,\n .sheet-toggle-switch-knob,\n .chat-input {\n animation: none;\n transition: none;\n }\n}\n";
5
+ export declare const mobileCss = "/* Mobile surface chrome: standalone stylesheet (no main-UI design tokens \u2014\n this page boots without the shell). Light-first palette; the dark palette\n applies only when the header toggle sets [data-theme='dark']. */\n\n:root {\n --m-bg: #f3f5f9;\n --m-bg-raised: #ffffff;\n --m-bg-input: #ffffff;\n --m-border: #dfe4ec;\n --m-text: #1c2333;\n --m-text-secondary: #4d5768;\n --m-text-tertiary: #8b95a7;\n --m-accent: #2f6fed;\n --m-accent-soft: rgba(47, 111, 237, 0.12);\n --m-danger: #d64545;\n --m-success: #1e9e6a;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);\n --m-shadow-raise: 0 6px 20px rgba(16, 24, 40, 0.1);\n --m-backdrop: rgba(15, 23, 42, 0.4);\n}\n\n:root[data-theme='dark'] {\n --m-bg: #111418;\n --m-bg-raised: #1a1f26;\n --m-bg-input: #20262e;\n --m-border: #2b333d;\n --m-text: #e8eaed;\n --m-text-secondary: #9aa3ad;\n --m-text-tertiary: #6b747f;\n --m-accent: #4f8cff;\n --m-accent-soft: rgba(79, 140, 255, 0.14);\n --m-danger: #e5534b;\n --m-success: #3fb68b;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n --m-shadow-raise: 0 6px 20px rgba(0, 0, 0, 0.4);\n --m-backdrop: rgba(0, 0, 0, 0.55);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n height: 100%;\n background: var(--m-bg);\n color: var(--m-text);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC', 'Segoe UI', sans-serif;\n font-size: 15px;\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n}\n\n/* The app fills exactly one viewport: the page itself never scrolls, each\n view owns its scroll region (chat history, session lists) and the chat\n composer stays pinned to the bottom of the screen. */\n#root {\n height: 100dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.mobile {\n min-height: 0;\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* \u2500\u2500 header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-header {\n position: sticky;\n top: 0;\n z-index: 10;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: calc(env(safe-area-inset-top, 0px) + 12px) 16px 10px;\n background: color-mix(in srgb, var(--m-bg) 88%, transparent);\n backdrop-filter: blur(10px);\n border-bottom: 1px solid var(--m-border);\n}\n\n.mobile-title {\n margin: 0;\n font-size: 17px;\n font-weight: 600;\n}\n\n.mobile-titleInline {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mobile-back {\n flex: none;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text);\n font-size: 22px;\n line-height: 1;\n cursor: pointer;\n transition: background-color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-back:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-back:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle {\n flex: none;\n margin-left: auto;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text-secondary);\n cursor: pointer;\n transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-theme-toggle:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-theme-toggle:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle svg {\n width: 18px;\n height: 18px;\n}\n\n/* \u2500\u2500 lists \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n list-style: none;\n margin: 0;\n padding: 8px 0 calc(env(safe-area-inset-bottom, 0px) + 16px);\n}\n\n.mobile-row {\n display: flex;\n align-items: center;\n gap: 10px;\n width: calc(100% - 24px);\n margin: 0 12px 8px;\n padding: 13px 14px;\n border: 1px solid var(--m-border);\n border-radius: 14px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.mobile-row:active {\n background: color-mix(in srgb, var(--m-text) 5%, var(--m-bg-raised));\n}\n\n.mobile-rowMain {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.mobile-rowTitle {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 15px;\n}\n\n.mobile-rowMeta {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.mobile-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 18px;\n}\n\n.mobile-live {\n margin-left: 6px;\n color: var(--m-success);\n font-size: 10px;\n vertical-align: middle;\n}\n\n/* \u2500\u2500 empty / error states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px;\n}\n\n.mobile-muted {\n margin: 0;\n color: var(--m-text-tertiary);\n}\n\n.mobile-error {\n margin: 0;\n color: var(--m-danger);\n}\n\n.mobile-pad {\n padding: 8px 16px;\n}\n\n.mobile-create {\n display: grid;\n gap: 8px;\n}\n\n.mobile-preset {\n display: grid;\n gap: 4px;\n}\n\n.mobile-presetLabel {\n color: var(--m-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.mobile-presetSelect {\n width: 100%;\n height: 40px;\n padding: 0 36px 0 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n}\n\n.mobile-presetSelect:disabled {\n opacity: 0.6;\n}\n\n.mobile-presetDescription {\n margin: 0;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-button {\n height: 38px;\n padding: 0 18px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.mobile-button:disabled {\n opacity: 0.5;\n}\n\n.mobile-block {\n display: block;\n width: calc(100% - 32px);\n margin: 4px 16px;\n}\n\n/* Primary action (create session): accent fill, readable on both themes. */\n.mobile-new {\n display: block;\n width: 100%;\n height: 44px;\n border: none;\n border-radius: 12px;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(47, 111, 237, 0.3);\n}\n\n.mobile-new:active {\n opacity: 0.88;\n}\n\n.mobile-new:disabled {\n opacity: 0.6;\n}\n\n.mobile-hint {\n display: block;\n margin-top: 2px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 chat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.chat-scroll {\n flex: 1;\n overflow-y: auto;\n padding: 12px 14px calc(env(safe-area-inset-bottom, 0px) + 12px);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-msg {\n max-width: 88%;\n padding: 10px 12px;\n border-radius: var(--m-radius);\n font-size: 14.5px;\n line-height: 1.55;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.chat-msg-user {\n align-self: flex-end;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n border-bottom-right-radius: 4px;\n box-shadow: 0 1px 3px rgba(47, 111, 237, 0.28);\n}\n\n.chat-msg-assistant {\n align-self: flex-start;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom-left-radius: 4px;\n box-shadow: var(--m-shadow);\n}\n\n.chat-msg-pending .chat-msg-text::after {\n content: '\u258D';\n animation: chat-blink 1s steps(2) infinite;\n}\n\n@keyframes chat-blink {\n 50% {\n opacity: 0;\n }\n}\n\n.chat-msg-failed {\n border-color: var(--m-danger);\n}\n\n.chat-msg-failtag {\n display: inline-block;\n margin-top: 6px;\n padding: 1px 8px;\n border: 1px solid var(--m-danger);\n border-radius: 999px;\n color: var(--m-danger);\n font-size: 11px;\n}\n\n.chat-msg-time {\n display: block;\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 10.5px;\n line-height: 1.2;\n}\n\n.chat-msg-toggle {\n display: block;\n margin-top: 8px;\n padding: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-accent);\n font-size: 13px;\n cursor: pointer;\n}\n\n/* Assistant markdown content (GFM subset; renderer in markdown.ts). */\n.chat-md {\n white-space: normal;\n}\n.chat-md-collapsed {\n max-height: 45vh;\n overflow: hidden;\n position: relative;\n}\n.chat-md-collapsed::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 28px;\n background: linear-gradient(transparent, var(--m-bg-raised));\n pointer-events: none;\n}\n.chat-md-body p { margin: 0 0 8px; }\n.chat-md-body p:last-child { margin-bottom: 0; }\n.chat-md-body h1, .chat-md-body h2, .chat-md-body h3,\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 {\n margin: 12px 0 6px;\n font-weight: 650;\n line-height: 1.3;\n}\n.chat-md-body h1 { font-size: 1.35em; }\n.chat-md-body h2 { font-size: 1.25em; }\n.chat-md-body h3 { font-size: 1.15em; }\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 { font-size: 1.05em; }\n.chat-md-body code {\n font-family: var(--m-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: 0.9em;\n background: var(--m-accent-soft);\n padding: 1px 5px;\n border-radius: 4px;\n}\n.chat-md-body pre {\n margin: 8px 0;\n padding: 10px;\n background: #101726;\n color: #dbe4f5;\n border-radius: 8px;\n overflow-x: auto;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.chat-md-body pre code {\n background: transparent;\n padding: 0;\n color: inherit;\n}\n.chat-md-body ul, .chat-md-body ol { margin: 4px 0 8px; padding-left: 22px; }\n.chat-md-body li { margin: 2px 0; }\n.chat-md-body blockquote {\n margin: 8px 0;\n padding: 4px 10px;\n border-left: 3px solid var(--m-accent);\n background: var(--m-accent-soft);\n border-radius: 0 6px 6px 0;\n color: var(--m-text-secondary);\n}\n.chat-md-body table {\n margin: 8px 0;\n border-collapse: collapse;\n display: block;\n overflow-x: auto;\n font-size: 13px;\n}\n.chat-md-body th, .chat-md-body td {\n border: 1px solid var(--m-border);\n padding: 4px 8px;\n}\n.chat-md-body th { background: var(--m-accent-soft); }\n.chat-md-body a { color: var(--m-accent); }\n.chat-md-body hr { border: none; border-top: 1px solid var(--m-border); margin: 10px 0; }\n.chat-md-body img { max-width: 100%; border-radius: 8px; }\n\n/* \u2500\u2500 message disclosures (reasoning / tools) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-disclosure {\n margin-bottom: 8px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: color-mix(in srgb, var(--m-bg) 55%, transparent);\n overflow: hidden;\n}\n\n.chat-disclosure-head {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 7px 10px;\n border: none;\n background: transparent;\n color: var(--m-text);\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.chat-disclosure-caret {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n line-height: 1;\n transition: transform 0.15s ease;\n}\n\n.chat-disclosure-open .chat-disclosure-caret {\n transform: rotate(90deg);\n}\n\n.chat-disclosure-label {\n flex: none;\n color: var(--m-text-secondary);\n}\n\n.chat-reasoning[data-pending] .chat-disclosure-label {\n color: var(--m-accent);\n}\n\n.chat-disclosure-summary {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-disclosure-count {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-disclosure-body {\n padding: 8px 10px 10px;\n border-top: 1px dashed var(--m-border);\n color: var(--m-text-secondary);\n font-size: 12.5px;\n line-height: 1.6;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n max-height: 40vh;\n overflow-y: auto;\n}\n\n.chat-tools-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.chat-tool-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.chat-tool-name {\n color: var(--m-text);\n font-size: 12.5px;\n font-weight: 600;\n}\n\n.chat-tool-args {\n margin: 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--m-bg-input);\n color: var(--m-text-secondary);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n/* \u2500\u2500 composer toolbar (model / permission chips) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-tools {\n display: flex;\n gap: 8px;\n padding: 6px 12px 0;\n}\n\n.chat-chip {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n font-size: 12px;\n cursor: pointer;\n}\n\n.chat-chip:active {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-chip-label {\n flex: none;\n color: var(--m-text-tertiary);\n}\n\n.chat-chip-value {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.chat-chip-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n}\n\n/* \u2500\u2500 bottom sheets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-backdrop {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n background: var(--m-backdrop);\n animation: sheet-fade 0.18s ease;\n}\n\n@keyframes sheet-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.sheet {\n max-height: 74dvh;\n display: flex;\n flex-direction: column;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom: none;\n border-radius: 16px 16px 0 0;\n box-shadow: var(--m-shadow-raise);\n padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);\n animation: sheet-up 0.22s ease;\n}\n\n@keyframes sheet-up {\n from {\n transform: translateY(40px);\n opacity: 0.6;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n.sheet-handle {\n flex: none;\n align-self: center;\n width: 36px;\n height: 4px;\n margin: 8px 0 4px;\n border-radius: 999px;\n background: var(--m-border);\n}\n\n.sheet-title {\n flex: none;\n padding: 6px 16px 10px;\n font-size: 15px;\n font-weight: 600;\n}\n\n.sheet-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 12px 8px;\n}\n\n.sheet-status {\n padding: 18px 8px;\n color: var(--m-text-tertiary);\n font-size: 13px;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n}\n\n.sheet-status-error {\n color: var(--m-danger);\n}\n\n.sheet-hint {\n margin: 0;\n color: var(--m-text-secondary);\n font-size: 12px;\n line-height: 1.5;\n max-width: 32em;\n}\n\n.sheet-error {\n margin: 0 0 8px;\n color: var(--m-danger);\n font-size: 12.5px;\n}\n\n.sheet-section {\n margin-bottom: 14px;\n}\n\n.sheet-section-title {\n padding: 4px 8px 6px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.sheet-option {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 10px 12px;\n border: 1px solid transparent;\n border-radius: 10px;\n background: transparent;\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.sheet-option:active {\n background: var(--m-bg-input);\n}\n\n.sheet-option-selected {\n border-color: var(--m-accent);\n background: color-mix(in srgb, var(--m-accent) 12%, transparent);\n}\n\n.sheet-option:disabled {\n opacity: 0.5;\n}\n\n.sheet-option-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-option-title {\n font-size: 14px;\n}\n\n.sheet-option-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-option-check {\n flex: none;\n color: var(--m-accent);\n font-size: 15px;\n font-weight: 700;\n}\n\n.sheet-confirm-desc {\n margin: 0 4px 14px;\n color: var(--m-text-secondary);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.sheet-confirm-actions {\n display: flex;\n gap: 10px;\n padding: 4px;\n}\n\n.sheet-confirm-actions .mobile-button {\n flex: 1;\n}\n\n.sheet-confirm-danger {\n flex: 1;\n height: 38px;\n border: none;\n border-radius: 10px;\n background: var(--m-danger);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.sheet-confirm-danger:disabled {\n opacity: 0.5;\n}\n\n.chat-meta {\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-typing {\n color: var(--m-text-tertiary);\n font-size: 13px;\n padding: 4px 2px;\n}\n\n.chat-inputbar {\n display: flex;\n gap: 8px;\n padding: 10px 12px calc(env(safe-area-inset-bottom, 0px) + 10px);\n border-top: 1px solid var(--m-border);\n background: var(--m-bg);\n}\n\n.chat-input {\n flex: 1;\n min-width: 0;\n min-height: 40px;\n max-height: 120px;\n /* Grow with the content (still bounded by min/max-height); browsers\n without field-sizing support ignore this line and keep the old size. */\n field-sizing: content;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n font-size: 14.5px;\n resize: none;\n outline: none;\n}\n\n.chat-input:focus {\n border-color: var(--m-accent);\n}\n\n.chat-send {\n flex: none;\n align-self: flex-end;\n height: 40px;\n padding: 0 16px;\n border: none;\n border-radius: 10px;\n background: var(--m-accent);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.chat-send:disabled {\n opacity: 0.5;\n}\n\n.chat-load-older {\n align-self: center;\n margin: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-text-secondary);\n font-size: 13px;\n cursor: pointer;\n padding: 6px 10px;\n}\n\n/* Touch surfaces have no hover, so keyboard focus must be fully visible.\n One shared ring keeps every interactive control consistent and readable\n on both palettes. */\n.mobile-row:focus-visible,\n.mobile-button:focus-visible,\n.mobile-new:focus-visible,\n.mobile-presetSelect:focus-visible,\n.chat-send:focus-visible,\n.chat-chip:focus-visible,\n.chat-msg-toggle:focus-visible,\n.chat-load-older:focus-visible,\n.chat-disclosure-head:focus-visible,\n.sheet-option:focus-visible,\n.sheet-confirm-danger:focus-visible,\n.sheet-toggle-switch:focus-visible,\n.chat-input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n/* \u2500\u2500 chat context usage chip (chat-tools row) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-context {\n flex: none;\n align-self: center;\n padding: 2px 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: color-mix(in srgb, var(--m-text-tertiary) 10%, transparent);\n color: var(--m-text-tertiary);\n font-size: 11px;\n line-height: 1.6;\n white-space: nowrap;\n}\n\n.chat-context-warn {\n border-color: var(--m-danger);\n background: color-mix(in srgb, var(--m-danger) 14%, transparent);\n color: var(--m-danger);\n}\n\n/* \u2500\u2500 display sheet toggle rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-toggle-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 8px;\n border-bottom: 1px solid var(--m-border);\n}\n\n.sheet-toggle-row:last-child {\n border-bottom: none;\n}\n\n.sheet-toggle-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-toggle-title {\n font-size: 14px;\n}\n\n.sheet-toggle-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-toggle-switch {\n position: relative;\n flex: none;\n width: 44px;\n height: 26px;\n border: none;\n border-radius: 999px;\n background: var(--m-border);\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.sheet-toggle-switch-on {\n background: var(--m-accent);\n}\n\n.sheet-toggle-switch-knob {\n position: absolute;\n top: 3px;\n left: 3px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 2px rgba(16, 24, 40, 0.2);\n transition: transform 0.15s ease;\n}\n\n.sheet-toggle-switch-on .sheet-toggle-switch-knob {\n transform: translateX(18px);\n}\n\n/* Accessibility: collapse every interaction/enter animation for\n reduced-motion users; surface layout itself stays static. */\n@media (prefers-reduced-motion: reduce) {\n .mobile-theme-toggle,\n .mobile-back,\n .mobile-row,\n .mobile-button,\n .mobile-new,\n .chat-send,\n .chat-chip,\n .chat-msg-toggle,\n .chat-load-older,\n .chat-disclosure-head,\n .chat-disclosure-caret,\n .chat-msg-pending .chat-msg-text::after,\n .sheet-backdrop,\n .sheet,\n .sheet-option,\n .sheet-confirm-danger,\n .sheet-toggle-switch,\n .sheet-toggle-switch-knob,\n .chat-input {\n animation: none;\n transition: none;\n }\n}\n\n/* \u2500\u2500 installed-app pairing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-pair {\n min-height: 100dvh;\n display: grid;\n place-items: center;\n padding: 24px;\n}\n\n.mobile-pairCard {\n display: grid;\n width: min(100%, 420px);\n gap: 12px;\n padding: 24px;\n border: 1px solid var(--m-border);\n border-radius: 12px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow-raise);\n}\n\n.mobile-pairLabel {\n color: var(--m-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.mobile-pairInput {\n width: 100%;\n min-height: 42px;\n box-sizing: border-box;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n outline: none;\n}\n\n.mobile-pairInput:focus-visible {\n border-color: var(--m-accent);\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-pairSubmit {\n width: 100%;\n}\n";
6
6
  //# sourceMappingURL=mobile-styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-styles.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,SAAS,g6tBA0/BrB,CAAA"}
1
+ {"version":3,"file":"mobile-styles.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,SAAS,mtxBAglCrB,CAAA"}
@@ -252,6 +252,43 @@ body {
252
252
  padding: 8px 16px;
253
253
  }
254
254
 
255
+ .mobile-create {
256
+ display: grid;
257
+ gap: 8px;
258
+ }
259
+
260
+ .mobile-preset {
261
+ display: grid;
262
+ gap: 4px;
263
+ }
264
+
265
+ .mobile-presetLabel {
266
+ color: var(--m-text-secondary);
267
+ font-size: 13px;
268
+ font-weight: 600;
269
+ }
270
+
271
+ .mobile-presetSelect {
272
+ width: 100%;
273
+ height: 40px;
274
+ padding: 0 36px 0 12px;
275
+ border: 1px solid var(--m-border);
276
+ border-radius: 10px;
277
+ background: var(--m-bg-input);
278
+ color: var(--m-text);
279
+ font: inherit;
280
+ }
281
+
282
+ .mobile-presetSelect:disabled {
283
+ opacity: 0.6;
284
+ }
285
+
286
+ .mobile-presetDescription {
287
+ margin: 0;
288
+ color: var(--m-text-tertiary);
289
+ font-size: 12px;
290
+ }
291
+
255
292
  /* ── buttons ─────────────────────────────────────────────────────────── */
256
293
 
257
294
  .mobile-button {
@@ -896,6 +933,7 @@ body {
896
933
  .mobile-row:focus-visible,
897
934
  .mobile-button:focus-visible,
898
935
  .mobile-new:focus-visible,
936
+ .mobile-presetSelect:focus-visible,
899
937
  .chat-send:focus-visible,
900
938
  .chat-chip:focus-visible,
901
939
  .chat-msg-toggle:focus-visible,
@@ -1020,4 +1058,52 @@ body {
1020
1058
  transition: none;
1021
1059
  }
1022
1060
  }
1061
+
1062
+ /* ── installed-app pairing ───────────────────────────────────────────── */
1063
+
1064
+ .mobile-pair {
1065
+ min-height: 100dvh;
1066
+ display: grid;
1067
+ place-items: center;
1068
+ padding: 24px;
1069
+ }
1070
+
1071
+ .mobile-pairCard {
1072
+ display: grid;
1073
+ width: min(100%, 420px);
1074
+ gap: 12px;
1075
+ padding: 24px;
1076
+ border: 1px solid var(--m-border);
1077
+ border-radius: 12px;
1078
+ background: var(--m-bg-raised);
1079
+ box-shadow: var(--m-shadow-raise);
1080
+ }
1081
+
1082
+ .mobile-pairLabel {
1083
+ color: var(--m-text-secondary);
1084
+ font-size: 13px;
1085
+ font-weight: 600;
1086
+ }
1087
+
1088
+ .mobile-pairInput {
1089
+ width: 100%;
1090
+ min-height: 42px;
1091
+ box-sizing: border-box;
1092
+ padding: 9px 12px;
1093
+ border: 1px solid var(--m-border);
1094
+ border-radius: 8px;
1095
+ background: var(--m-bg-input);
1096
+ color: var(--m-text);
1097
+ font: inherit;
1098
+ outline: none;
1099
+ }
1100
+
1101
+ .mobile-pairInput:focus-visible {
1102
+ border-color: var(--m-accent);
1103
+ box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);
1104
+ }
1105
+
1106
+ .mobile-pairSubmit {
1107
+ width: 100%;
1108
+ }
1023
1109
  `;
@@ -0,0 +1,31 @@
1
+ /** Pairing helpers owned by the standalone /m mobile surface. */
2
+ export type PairFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ export interface MobilePairInput {
4
+ token: string;
5
+ workspaceId?: string;
6
+ }
7
+ export type MobilePairAccept = {
8
+ ok: true;
9
+ } | {
10
+ ok: false;
11
+ message: string;
12
+ };
13
+ export type MobilePairBootstrap = {
14
+ kind: 'none';
15
+ } | {
16
+ kind: 'accepted';
17
+ path: string;
18
+ } | {
19
+ kind: 'failed';
20
+ path: string;
21
+ message: string;
22
+ };
23
+ /** Parse a pairing token or a copied pairing link without following its origin. */
24
+ export declare function parseMobilePairInput(value: string): MobilePairInput | undefined;
25
+ /** Build the safe, token-free mobile destination after successful pairing. */
26
+ export declare function mobilePairPath(workspaceId?: string): string;
27
+ /** Accept one pairing token on this exact browser or installed-web-app context. */
28
+ export declare function acceptMobilePair(token: string, fetcher?: PairFetch): Promise<MobilePairAccept>;
29
+ /** Consume a QR pairing token before the mobile application starts making RPC calls. */
30
+ export declare function consumeMobilePairUrl(href: string, fetcher?: PairFetch): Promise<MobilePairBootstrap>;
31
+ //# sourceMappingURL=pairing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../../src/mobile/pairing.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE3F,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAElC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAIrD,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAgB/E;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,mFAAmF;AACnF,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,SAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAc3G;AAED,wFAAwF;AACxF,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,SAAiB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAejH"}
@@ -0,0 +1,62 @@
1
+ const MOBILE_ROOT = '/m/';
2
+ /** Parse a pairing token or a copied pairing link without following its origin. */
3
+ export function parseMobilePairInput(value) {
4
+ const trimmed = value.trim();
5
+ if (trimmed === '')
6
+ return undefined;
7
+ try {
8
+ const url = new URL(trimmed);
9
+ const token = url.searchParams.get('pair');
10
+ if (token === null || token === '')
11
+ return undefined;
12
+ const workspaceId = url.searchParams.get('workspace');
13
+ return {
14
+ token,
15
+ ...(workspaceId !== null && workspaceId !== '' ? { workspaceId } : {}),
16
+ };
17
+ }
18
+ catch {
19
+ return { token: trimmed };
20
+ }
21
+ }
22
+ /** Build the safe, token-free mobile destination after successful pairing. */
23
+ export function mobilePairPath(workspaceId) {
24
+ return workspaceId === undefined ? MOBILE_ROOT : MOBILE_ROOT + '?workspace=' + encodeURIComponent(workspaceId);
25
+ }
26
+ /** Accept one pairing token on this exact browser or installed-web-app context. */
27
+ export async function acceptMobilePair(token, fetcher = fetch) {
28
+ try {
29
+ const response = await fetcher('/api/pair/accept', {
30
+ method: 'POST',
31
+ headers: { 'content-type': 'application/json' },
32
+ body: JSON.stringify({ token }),
33
+ });
34
+ if (response.ok)
35
+ return { ok: true };
36
+ if (response.status === 404)
37
+ return { ok: false, message: '配对链接无效或已过期。' };
38
+ if (response.status === 409)
39
+ return { ok: false, message: '配对链接已被使用。' };
40
+ return { ok: false, message: '此设备无法使用该配对链接。' };
41
+ }
42
+ catch {
43
+ return { ok: false, message: '无法连接到配对服务。' };
44
+ }
45
+ }
46
+ /** Consume a QR pairing token before the mobile application starts making RPC calls. */
47
+ export async function consumeMobilePairUrl(href, fetcher = fetch) {
48
+ let url;
49
+ try {
50
+ url = new URL(href);
51
+ }
52
+ catch {
53
+ return { kind: 'none' };
54
+ }
55
+ const token = url.searchParams.get('pair');
56
+ if (token === null || token === '')
57
+ return { kind: 'none' };
58
+ const workspaceId = url.searchParams.get('workspace');
59
+ const path = mobilePairPath(workspaceId === null || workspaceId === '' ? undefined : workspaceId);
60
+ const result = await acceptMobilePair(token, fetcher);
61
+ return result.ok ? { kind: 'accepted', path } : { kind: 'failed', path, message: result.message };
62
+ }
@@ -29,11 +29,18 @@ export declare function toSessionView(item: {
29
29
  }): SessionView;
30
30
  /** Human clock, e.g. "14:05" or "昨天 20:31". */
31
31
  export declare function formatTime(epochMs: number): string;
32
- /**
33
- * The surface root.
34
- * @returns the app tree.
35
- */
36
- export declare function App(): import("react").JSX.Element;
32
+ /** Props accepted by the mobile root before it begins paired-device RPC calls. */
33
+ export interface AppProps {
34
+ initialPairError?: string;
35
+ }
36
+ /** Whether the mobile gateway rejected this browser for lack of a paired cookie. */
37
+ export declare function isUnpairedMobileError(error: unknown): boolean;
38
+ /** The result of probing the paired-device-only mobile preference endpoint. */
39
+ export type MobilePairState = 'checking' | 'paired' | 'unpaired' | 'unavailable';
40
+ /** Classify an initial mobile preference failure without treating outage as authorization. */
41
+ export declare function mobilePairStateForError(error: unknown): Extract<MobilePairState, 'unpaired' | 'unavailable'>;
42
+ /** Gate the independent mobile bundle until its own browser context is paired. */
43
+ export declare function App({ initialPairError }: AppProps): import("react").JSX.Element;
37
44
  /** Shared error text for the surface's small failure affordances. */
38
45
  export declare function errorText(error: unknown): string;
39
46
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAA2B,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAazF,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CACnD,GAAG,WAAW,CAad;AAED,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASlD;AAED;;;GAGG;AACH,wBAAgB,GAAG,gCA4DlB;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKhD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIjE;AAED,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,4CAEtF;AAED,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,CAAA"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAmD,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAcjH,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CACnD,GAAG,WAAW,CAad;AAED,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASlD;AAED,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAA;AAEhF,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,EAAE,UAAU,GAAG,aAAa,CAAC,CAE5G;AAED,kFAAkF;AAClF,wBAAgB,GAAG,CAAC,EAAE,gBAAgB,EAAE,EAAE,QAAQ,+BA4BjD;AAiED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKhD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIjE;AAED,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,4CAEtF;AAED,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,CAAA"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Mobile surface root: the view state machine (workspaces → sessions →
4
4
  * chat) and the top-level data flows. Deliberately plain React state — no
@@ -6,12 +6,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  * affordance, and every piece of data is fetched on demand.
7
7
  */
8
8
  import { useCallback, useEffect, useRef, useState } from 'react';
9
- import { history as fetchHistory, listSessions, listWorkspaces, prompt } from '../api.js';
9
+ import { fetchMobilePreferences, history as fetchHistory, listSessions, listWorkspaces, prompt } from '../api.js';
10
10
  import { MuxClient } from '../mux.js';
11
11
  import { RpcCallError, RpcTransportError } from '../rpc.js';
12
12
  import { ChatView } from './ChatView.js';
13
13
  import { SessionListView } from './SessionListView.js';
14
14
  import { WorkspaceView as WorkspaceRoster } from './WorkspaceView.js';
15
+ import { PairRequiredView } from '../PairRequiredView.js';
15
16
  /** Read the optional workspace target carried from the pairing QR flow. */
16
17
  export function mobileWorkspaceTarget(search) {
17
18
  const value = new URLSearchParams(search).get('workspace');
@@ -45,11 +46,37 @@ export function formatTime(epochMs) {
45
46
  return `昨天 ${clock}`;
46
47
  return `${String(date.getMonth() + 1)}月${String(date.getDate())}日 ${clock}`;
47
48
  }
48
- /**
49
- * The surface root.
50
- * @returns the app tree.
51
- */
52
- export function App() {
49
+ /** Whether the mobile gateway rejected this browser for lack of a paired cookie. */
50
+ export function isUnpairedMobileError(error) {
51
+ return error instanceof RpcTransportError && error.message === 'HTTP 403';
52
+ }
53
+ /** Classify an initial mobile preference failure without treating outage as authorization. */
54
+ export function mobilePairStateForError(error) {
55
+ return isUnpairedMobileError(error) ? 'unpaired' : 'unavailable';
56
+ }
57
+ /** Gate the independent mobile bundle until its own browser context is paired. */
58
+ export function App({ initialPairError }) {
59
+ const [pairState, setPairState] = useState('checking');
60
+ useEffect(() => {
61
+ let current = true;
62
+ void fetchMobilePreferences().then(() => { if (current)
63
+ setPairState('paired'); }, (error) => { if (current)
64
+ setPairState(mobilePairStateForError(error)); });
65
+ return () => { current = false; };
66
+ }, []);
67
+ if (pairState === 'checking') {
68
+ return _jsx("main", { className: "mobile mobile-empty", children: _jsx("p", { className: "mobile-muted", children: "\u6B63\u5728\u8FDE\u63A5..." }) });
69
+ }
70
+ if (pairState === 'unpaired') {
71
+ return _jsx(PairRequiredView, { initialError: initialPairError, onPaired: (path) => { window.location.replace(path); } });
72
+ }
73
+ if (pairState === 'unavailable') {
74
+ return (_jsxs("main", { className: "mobile mobile-empty", children: [_jsx("p", { className: "mobile-error", role: "alert", children: "\u65E0\u6CD5\u8FDE\u63A5\u5230\u8FD0\u884C\u4E2D\u7684 DSH host\u3002" }), _jsx("button", { className: "mobile-new", type: "button", onClick: () => { window.location.reload(); }, children: "\u91CD\u8BD5" })] }));
75
+ }
76
+ return _jsx(PairedApp, {});
77
+ }
78
+ /** The existing remote mobile surface, mounted only after device pairing succeeds. */
79
+ function PairedApp() {
53
80
  const [route, setRoute] = useState({ kind: 'workspaces' });
54
81
  const [initialWorkspaceId, setInitialWorkspaceId] = useState(() => mobileWorkspaceTarget(window.location.search));
55
82
  const muxRef = useRef(undefined);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=App.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.test.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment jsdom
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { cleanup, render, screen, waitFor } from '@testing-library/react';
5
+ import { RpcTransportError } from '../rpc.js';
6
+ const api = vi.hoisted(() => ({
7
+ fetchMobilePreferences: vi.fn(),
8
+ history: vi.fn(),
9
+ listSessions: vi.fn(),
10
+ listWorkspaces: vi.fn(),
11
+ prompt: vi.fn(),
12
+ }));
13
+ vi.mock('../api.ts', () => api);
14
+ vi.mock('../mux.ts', () => ({
15
+ MuxClient: class {
16
+ start() { }
17
+ stop() { }
18
+ observe() { }
19
+ },
20
+ }));
21
+ vi.mock('./WorkspaceView.tsx', () => ({ WorkspaceView: () => _jsx("div", { children: "workspace-ready" }) }));
22
+ vi.mock('./SessionListView.tsx', () => ({ SessionListView: () => _jsx("div", { children: "sessions-ready" }) }));
23
+ vi.mock('./ChatView.tsx', () => ({ ChatView: () => _jsx("div", { children: "chat-ready" }) }));
24
+ import { App, mobilePairStateForError } from './App.js';
25
+ afterEach(() => {
26
+ cleanup();
27
+ vi.clearAllMocks();
28
+ });
29
+ describe('mobile paired-device gate', () => {
30
+ it('distinguishes a missing paired cookie from a transport outage', () => {
31
+ expect(mobilePairStateForError(new RpcTransportError('HTTP 403'))).toBe('unpaired');
32
+ expect(mobilePairStateForError(new RpcTransportError('HTTP 503'))).toBe('unavailable');
33
+ expect(mobilePairStateForError(new Error('offline'))).toBe('unavailable');
34
+ });
35
+ it('keeps a valid paired context usable after a failed QR token', async () => {
36
+ api.fetchMobilePreferences.mockResolvedValue({ mobileEnterToSend: true });
37
+ render(_jsx(App, { initialPairError: "\u914D\u5BF9\u94FE\u63A5\u5DF2\u88AB\u4F7F\u7528\u3002" }));
38
+ await waitFor(() => expect(screen.getByText('workspace-ready')).toBeDefined());
39
+ expect(screen.queryByRole('alert')).toBeNull();
40
+ });
41
+ it('shows the pairing screen only when the mobile gateway rejects the device', async () => {
42
+ api.fetchMobilePreferences.mockRejectedValue(new RpcTransportError('HTTP 403'));
43
+ render(_jsx(App, { initialPairError: "\u914D\u5BF9\u94FE\u63A5\u5DF2\u88AB\u4F7F\u7528\u3002" }));
44
+ await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('配对链接已被使用。'));
45
+ });
46
+ it('shows a retryable connection failure rather than mounting remote views on outage', async () => {
47
+ api.fetchMobilePreferences.mockRejectedValue(new RpcTransportError('HTTP 503'));
48
+ render(_jsx(App, {}));
49
+ await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('无法连接到运行中的 DSH host。'));
50
+ expect(screen.queryByText('workspace-ready')).toBeNull();
51
+ });
52
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAKjE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGrC,+BAA+B;AAC/B,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAA;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC3B,MAAM,IAAI,IAAI,CAAA;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAqCzC,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;CACrB;AAqCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,aAAa,+BAgW/D"}
1
+ {"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAKjE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGrC,+BAA+B;AAC/B,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAA;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC3B,MAAM,IAAI,IAAI,CAAA;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAqCzC,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;CACrB;AAqCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,aAAa,+BAmX/D"}
@@ -11,11 +11,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * - a toolbar above the composer carries the model (+ thinking effort) and
12
12
  * permission pickers, both as bottom sheets.
13
13
  */
14
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
14
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
15
  import { loadHistory, prompt } from './App.js';
16
16
  import { errorText, formatTime, staleHostHint } from './App.js';
17
17
  import { fetchMobilePreferences, models, selectModel, sendCommand } from '../api.js';
18
- import { foldEvents } from '../messages.js';
18
+ import { EventFolder, foldEvents } from '../messages.js';
19
19
  import { renderMarkdown } from '../markdown.js';
20
20
  import { ThemeToggle } from '../theme-toggle.js';
21
21
  /**
@@ -119,6 +119,8 @@ export function ChatView({ session, mux, onBack }) {
119
119
  const tailLoadingRef = useRef(true);
120
120
  /** Live session events buffered while the initial tail page loads. */
121
121
  const liveBufferRef = useRef([]);
122
+ /** Incremental folder for this session's stream (indexes stay hot across events). */
123
+ const folderRef = useRef(undefined);
122
124
  /** True once the live buffer hit its cap (oldest events were dropped). */
123
125
  const liveBufferOverflowRef = useRef(false);
124
126
  /** The session's permission select (absent = capability not composed). */
@@ -158,6 +160,7 @@ export function ChatView({ session, mux, onBack }) {
158
160
  tailLoadingRef.current = true;
159
161
  liveBufferRef.current = [];
160
162
  liveBufferOverflowRef.current = false;
163
+ folderRef.current = undefined;
161
164
  setLoading(true);
162
165
  setError(undefined);
163
166
  setMessages([]);
@@ -169,7 +172,9 @@ export function ChatView({ session, mux, onBack }) {
169
172
  const buffered = liveBufferRef.current;
170
173
  liveBufferRef.current = [];
171
174
  tailLoadingRef.current = false;
172
- setMessages(foldEvents(buffered, foldEvents(page.events.map(eventOf))));
175
+ const folder = new EventFolder(foldEvents(page.events.map(eventOf)));
176
+ folderRef.current = folder;
177
+ setMessages(folder.fold(buffered));
173
178
  setHasOlder(page.hasMore);
174
179
  setLoading(false);
175
180
  // The history-tail projection baseline seeds the permission picker.
@@ -185,7 +190,9 @@ export function ChatView({ session, mux, onBack }) {
185
190
  void loadHistory(session.sessionId, undefined, controller.signal).then((fresh) => {
186
191
  if (cancelled)
187
192
  return;
188
- setMessages(previous => foldEvents(fresh.events.map(eventOf), previous));
193
+ const folder = folderRef.current;
194
+ const freshEvents = fresh.events.map(eventOf);
195
+ setMessages(previous => folder === undefined ? foldEvents(freshEvents, previous) : folder.fold(freshEvents));
189
196
  liveBufferOverflowRef.current = false;
190
197
  }, (reason) => {
191
198
  if (cancelled)
@@ -200,8 +207,10 @@ export function ChatView({ session, mux, onBack }) {
200
207
  const buffered = liveBufferRef.current;
201
208
  liveBufferRef.current = [];
202
209
  tailLoadingRef.current = false;
203
- if (buffered.length > 0)
204
- setMessages(foldEvents(buffered));
210
+ if (buffered.length > 0) {
211
+ const folder = folderRef.current;
212
+ setMessages(folder === undefined ? foldEvents(buffered) : folder.fold(buffered));
213
+ }
205
214
  setError(errorText(reason));
206
215
  setLoading(false);
207
216
  });
@@ -240,7 +249,10 @@ export function ChatView({ session, mux, onBack }) {
240
249
  liveBufferRef.current.push(event);
241
250
  return;
242
251
  }
243
- setMessages(previous => foldEvents([event], previous));
252
+ setMessages(previous => {
253
+ const folder = folderRef.current;
254
+ return folder === undefined ? foldEvents([event], previous) : folder.fold([event]);
255
+ });
244
256
  return;
245
257
  }
246
258
  // Live projection pushes keep the permission picker current.
@@ -295,7 +307,14 @@ export function ChatView({ session, mux, onBack }) {
295
307
  pendingRef.current = false;
296
308
  setLoading(false);
297
309
  const older = foldEvents(page.events.map(eventOf));
298
- setMessages(previous => [...older, ...previous]);
310
+ const folder = folderRef.current;
311
+ if (folder === undefined) {
312
+ setMessages(previous => [...older, ...previous]);
313
+ }
314
+ else {
315
+ folder.prepend(older);
316
+ setMessages(folder.snapshot());
317
+ }
299
318
  setHasOlder(page.hasMore);
300
319
  }, (reason) => {
301
320
  pendingRef.current = false;
@@ -350,8 +369,12 @@ export function ChatView({ session, mux, onBack }) {
350
369
  }, onClose: () => { setSheet(null); } })), sheet === 'display' && (_jsx(DisplaySheet, { showToolCalls: showToolCalls, showSystemMessages: showSystemMessages, onToolCalls: (value) => { setShowToolCalls(value); writeStoredBoolean(SHOW_TOOL_CALLS_KEY, value); }, onSystemMessages: (value) => { setShowSystemMessages(value); writeStoredBoolean(SHOW_SYSTEM_MESSAGES_KEY, value); }, onClose: () => { setSheet(null); } }))] }));
351
370
  }
352
371
  /* ── message rows ─────────────────────────────────────────────────────── */
353
- /** One rendered message row (user bubble or assistant bubble with folds). */
354
- function MessageRow({ message, showToolCalls, showSystemMessages }) {
372
+ /**
373
+ * One rendered message row (user bubble or assistant bubble with folds).
374
+ * Memoized: live streaming updates exactly one message object per frame, so
375
+ * unchanged rows skip re-rendering their markdown/sub-components.
376
+ */
377
+ const MessageRow = memo(function MessageRow({ message, showToolCalls, showSystemMessages }) {
355
378
  // Injected user messages (sourceKind defined and not 'user') hide behind
356
379
  // the system-message toggle. Assistant messages are never hidden.
357
380
  if (message.kind === 'user'
@@ -363,7 +386,7 @@ function MessageRow({ message, showToolCalls, showSystemMessages }) {
363
386
  return (_jsxs("div", { className: `chat-msg chat-msg-${message.kind}${message.pending === true ? ' chat-msg-pending' : ''}${message.failed === true ? ' chat-msg-failed' : ''}`, children: [message.kind === 'assistant' && message.reasoning !== undefined && message.reasoning !== '' && (_jsx(ReasoningDisclosure, { text: message.reasoning, pending: message.pending === true })), showToolCalls && message.kind === 'assistant' && message.tools !== undefined && message.tools.length > 0 && (_jsx(ToolDisclosure, { tools: message.tools })), message.kind === 'assistant'
364
387
  ? _jsx(MarkdownText, { text: message.text })
365
388
  : _jsx(CollapsibleText, { text: message.text }), message.failed === true && _jsx("span", { className: "chat-msg-failtag", children: "\u672C\u6B21\u56DE\u590D\u5931\u8D25" }), _jsx("span", { className: "chat-msg-time", children: formatTime(message.time) })] }));
366
- }
389
+ });
367
390
  /** Collapsed-by-default reasoning disclosure (web-UI Think-row parity). */
368
391
  function ReasoningDisclosure({ text, pending }) {
369
392
  const [open, setOpen] = useState(false);
@@ -1 +1 @@
1
- {"version":3,"file":"SessionListView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAGjG,OAAO,EAAuD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAGjG,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,YAAY,CAAA;IACvB,MAAM,IAAI,IAAI,CAAA;IACd,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,+BAiJlF"}
1
+ {"version":3,"file":"SessionListView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAIjG,OAAO,EAAuD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAGjG,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,YAAY,CAAA;IACvB,MAAM,IAAI,IAAI,CAAA;IACd,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,+BAqMlF"}