@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.10

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 (238) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +77 -468
  3. package/README.zh.md +83 -113
  4. package/lib/client.js +993 -203
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1136 -1466
  7. package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
  8. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
  9. package/lib/types/client/FooterRemoteEntry.js +1 -1
  10. package/lib/types/client/RemoteEntry.d.ts +7 -4
  11. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  12. package/lib/types/client/RemoteEntry.js +7 -10
  13. package/lib/types/client/RemotePanel.d.ts +2 -2
  14. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  15. package/lib/types/client/RemotePanel.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
  17. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  18. package/lib/types/client/RemoteSettingsCard.js +56 -3
  19. package/lib/types/client/deep-link.d.ts +10 -13
  20. package/lib/types/client/deep-link.d.ts.map +1 -1
  21. package/lib/types/client/deep-link.js +14 -74
  22. package/lib/types/client/index.d.ts +2 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +79 -39
  25. package/lib/types/client/locales.d.ts +32 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +42 -22
  28. package/lib/types/client/mobile-adapt.d.ts +49 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +1002 -0
  31. package/lib/types/client/pair-api.d.ts +24 -6
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +9 -14
  34. package/lib/types/client/remote-channel.d.ts +3 -0
  35. package/lib/types/client/remote-channel.d.ts.map +1 -1
  36. package/lib/types/client/remote-channel.js +35 -3
  37. package/lib/types/client/settings-form.d.ts +35 -43
  38. package/lib/types/client/settings-form.d.ts.map +1 -1
  39. package/lib/types/client/settings-form.js +60 -53
  40. package/lib/types/firewall.d.ts +75 -0
  41. package/lib/types/firewall.d.ts.map +1 -0
  42. package/lib/types/firewall.js +184 -0
  43. package/lib/types/gate.d.ts +11 -5
  44. package/lib/types/gate.d.ts.map +1 -1
  45. package/lib/types/gate.js +11 -5
  46. package/lib/types/index.d.ts +39 -10
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +206 -58
  49. package/lib/types/inner-auth.d.ts +32 -0
  50. package/lib/types/inner-auth.d.ts.map +1 -0
  51. package/lib/types/inner-auth.js +72 -0
  52. package/lib/types/lan-bind-plan.d.ts +55 -0
  53. package/lib/types/lan-bind-plan.d.ts.map +1 -0
  54. package/lib/types/lan-bind-plan.js +60 -0
  55. package/lib/types/lan-bind.d.ts +69 -0
  56. package/lib/types/lan-bind.d.ts.map +1 -0
  57. package/lib/types/lan-bind.js +128 -0
  58. package/lib/types/loopback-proxy.d.ts +9 -2
  59. package/lib/types/loopback-proxy.d.ts.map +1 -1
  60. package/lib/types/loopback-proxy.js +24 -2
  61. package/lib/types/pairing.d.ts +15 -3
  62. package/lib/types/pairing.d.ts.map +1 -1
  63. package/lib/types/pairing.js +20 -6
  64. package/lib/types/remote-api.d.ts +30 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +55 -42
  67. package/lib/types/remote-channel-boot.d.ts +21 -2
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +65 -3
  70. package/lib/types/remote-channel-rules.d.ts +8 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +9 -2
  73. package/lib/types/remote-methods.d.ts +45 -11
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +56 -27
  76. package/lib/types/remote-presence-pet.d.ts +51 -0
  77. package/lib/types/remote-presence-pet.d.ts.map +1 -0
  78. package/lib/types/remote-presence-pet.js +101 -0
  79. package/lib/types/routes.d.ts +76 -4
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +336 -19
  82. package/lib/types/tunnel.d.ts.map +1 -1
  83. package/lib/types/tunnel.js +7 -1
  84. package/package.json +20 -18
  85. package/src/client/FooterRemoteEntry.tsx +9 -10
  86. package/src/client/RemoteEntry.tsx +13 -14
  87. package/src/client/RemotePanel.tsx +5 -16
  88. package/src/client/RemoteSettingsCard.tsx +72 -13
  89. package/src/client/deep-link.ts +14 -75
  90. package/src/client/index.ts +82 -39
  91. package/src/client/locales.ts +42 -22
  92. package/src/client/mobile-adapt.ts +954 -0
  93. package/src/client/pair-api.ts +26 -15
  94. package/src/client/remote-channel.ts +31 -3
  95. package/src/client/settings-form.ts +78 -92
  96. package/src/dsh-home.ts +1 -1
  97. package/src/firewall.ts +230 -0
  98. package/src/gate.ts +11 -5
  99. package/src/index.ts +235 -65
  100. package/src/inner-auth.ts +85 -0
  101. package/src/lan-bind-plan.ts +81 -0
  102. package/src/lan-bind.ts +145 -0
  103. package/src/loopback-proxy.ts +34 -0
  104. package/src/pairing.persist.test.ts +1 -1
  105. package/src/pairing.ts +20 -6
  106. package/src/remote-api.ts +67 -45
  107. package/src/remote-channel-boot.ts +70 -3
  108. package/src/remote-channel-rules.ts +17 -2
  109. package/src/remote-methods.ts +58 -27
  110. package/src/remote-presence-pet.ts +129 -0
  111. package/src/routes.ts +358 -19
  112. package/src/tunnel.ts +7 -1
  113. package/assets/apple-touch-icon.png +0 -0
  114. package/assets/mobile-icon-192.png +0 -0
  115. package/assets/mobile-icon-512.png +0 -0
  116. package/assets/mobile-service-worker.js +0 -63
  117. package/lib/mobile.js +0 -17658
  118. package/lib/mobile.js.map +0 -1
  119. package/lib/types/mobile/PairRequiredView.d.ts +0 -7
  120. package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
  121. package/lib/types/mobile/PairRequiredView.js +0 -28
  122. package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
  123. package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
  124. package/lib/types/mobile/PairRequiredView.test.js +0 -24
  125. package/lib/types/mobile/api.d.ts +0 -145
  126. package/lib/types/mobile/api.d.ts.map +0 -1
  127. package/lib/types/mobile/api.js +0 -103
  128. package/lib/types/mobile/index.d.ts +0 -10
  129. package/lib/types/mobile/index.d.ts.map +0 -1
  130. package/lib/types/mobile/index.js +0 -38
  131. package/lib/types/mobile/markdown.d.ts +0 -34
  132. package/lib/types/mobile/markdown.d.ts.map +0 -1
  133. package/lib/types/mobile/markdown.js +0 -285
  134. package/lib/types/mobile/markdown.test.d.ts +0 -2
  135. package/lib/types/mobile/markdown.test.d.ts.map +0 -1
  136. package/lib/types/mobile/markdown.test.js +0 -79
  137. package/lib/types/mobile/messages.d.ts +0 -149
  138. package/lib/types/mobile/messages.d.ts.map +0 -1
  139. package/lib/types/mobile/messages.js +0 -561
  140. package/lib/types/mobile/messages.test.d.ts +0 -2
  141. package/lib/types/mobile/messages.test.d.ts.map +0 -1
  142. package/lib/types/mobile/messages.test.js +0 -356
  143. package/lib/types/mobile/mobile-styles.d.ts +0 -6
  144. package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
  145. package/lib/types/mobile/mobile-styles.js +0 -1438
  146. package/lib/types/mobile/mobile-theme.d.ts +0 -22
  147. package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
  148. package/lib/types/mobile/mobile-theme.js +0 -70
  149. package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
  150. package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
  151. package/lib/types/mobile/mobile-theme.test.js +0 -71
  152. package/lib/types/mobile/mux.d.ts +0 -128
  153. package/lib/types/mobile/mux.d.ts.map +0 -1
  154. package/lib/types/mobile/mux.js +0 -305
  155. package/lib/types/mobile/mux.test.d.ts +0 -2
  156. package/lib/types/mobile/mux.test.d.ts.map +0 -1
  157. package/lib/types/mobile/mux.test.js +0 -267
  158. package/lib/types/mobile/pairing.d.ts +0 -31
  159. package/lib/types/mobile/pairing.d.ts.map +0 -1
  160. package/lib/types/mobile/pairing.js +0 -62
  161. package/lib/types/mobile/rpc.d.ts +0 -38
  162. package/lib/types/mobile/rpc.d.ts.map +0 -1
  163. package/lib/types/mobile/rpc.js +0 -83
  164. package/lib/types/mobile/theme-toggle.d.ts +0 -12
  165. package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
  166. package/lib/types/mobile/theme-toggle.js +0 -26
  167. package/lib/types/mobile/views/App.d.ts +0 -57
  168. package/lib/types/mobile/views/App.d.ts.map +0 -1
  169. package/lib/types/mobile/views/App.js +0 -150
  170. package/lib/types/mobile/views/App.test.d.ts +0 -2
  171. package/lib/types/mobile/views/App.test.d.ts.map +0 -1
  172. package/lib/types/mobile/views/App.test.js +0 -52
  173. package/lib/types/mobile/views/ChatView.d.ts +0 -63
  174. package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
  175. package/lib/types/mobile/views/ChatView.js +0 -821
  176. package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
  177. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
  178. package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
  179. package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
  180. package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
  181. package/lib/types/mobile/views/ChatView.test.js +0 -718
  182. package/lib/types/mobile/views/SessionListView.d.ts +0 -27
  183. package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
  184. package/lib/types/mobile/views/SessionListView.js +0 -139
  185. package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
  186. package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
  187. package/lib/types/mobile/views/SessionListView.test.js +0 -167
  188. package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
  189. package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
  190. package/lib/types/mobile/views/WorkspaceView.js +0 -93
  191. package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
  192. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
  193. package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
  194. package/lib/types/mobile-api.d.ts +0 -65
  195. package/lib/types/mobile-api.d.ts.map +0 -1
  196. package/lib/types/mobile-api.js +0 -404
  197. package/lib/types/mobile-pending.d.ts +0 -38
  198. package/lib/types/mobile-pending.d.ts.map +0 -1
  199. package/lib/types/mobile-pending.js +0 -59
  200. package/lib/types/mobile-pwa.d.ts +0 -6
  201. package/lib/types/mobile-pwa.d.ts.map +0 -1
  202. package/lib/types/mobile-pwa.js +0 -17
  203. package/lib/types/mobile-routes.d.ts +0 -13
  204. package/lib/types/mobile-routes.d.ts.map +0 -1
  205. package/lib/types/mobile-routes.js +0 -217
  206. package/lib/types/paired-model-catalog.d.ts +0 -17
  207. package/lib/types/paired-model-catalog.d.ts.map +0 -1
  208. package/lib/types/paired-model-catalog.js +0 -311
  209. package/src/mobile/PairRequiredView.test.tsx +0 -28
  210. package/src/mobile/PairRequiredView.tsx +0 -58
  211. package/src/mobile/api.ts +0 -229
  212. package/src/mobile/index.tsx +0 -41
  213. package/src/mobile/markdown.test.ts +0 -91
  214. package/src/mobile/markdown.ts +0 -291
  215. package/src/mobile/messages.test.ts +0 -402
  216. package/src/mobile/messages.ts +0 -685
  217. package/src/mobile/mobile-styles.ts +0 -1439
  218. package/src/mobile/mobile-theme.test.ts +0 -84
  219. package/src/mobile/mobile-theme.ts +0 -76
  220. package/src/mobile/mux.test.ts +0 -314
  221. package/src/mobile/mux.ts +0 -336
  222. package/src/mobile/pairing.ts +0 -77
  223. package/src/mobile/rpc.ts +0 -90
  224. package/src/mobile/theme-toggle.tsx +0 -47
  225. package/src/mobile/views/App.test.tsx +0 -62
  226. package/src/mobile/views/App.tsx +0 -213
  227. package/src/mobile/views/ChatView.stream.test.tsx +0 -268
  228. package/src/mobile/views/ChatView.test.tsx +0 -816
  229. package/src/mobile/views/ChatView.tsx +0 -1342
  230. package/src/mobile/views/SessionListView.test.tsx +0 -207
  231. package/src/mobile/views/SessionListView.tsx +0 -244
  232. package/src/mobile/views/WorkspaceView.test.tsx +0 -227
  233. package/src/mobile/views/WorkspaceView.tsx +0 -245
  234. package/src/mobile-api.ts +0 -418
  235. package/src/mobile-pending.ts +0 -88
  236. package/src/mobile-pwa.ts +0 -19
  237. package/src/mobile-routes.ts +0 -231
  238. package/src/paired-model-catalog.ts +0 -272
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { useCallback, useEffect, useRef, useState } from 'react'
11
11
  import { createPortal } from 'react-dom'
12
- import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
12
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
13
13
  import type { PairingPhase } from '../pairing.ts'
14
14
  import { RemotePanel, type PanelState } from './RemotePanel.tsx'
15
15
  import { copyText, issuePair, revokePair, stopPair, type DeviceFrame, type IssueResponse, type PairStateFrame, type TunnelStatusFrame } from './pair-api.ts'
@@ -17,8 +17,11 @@ import { PhoneIcon } from './PhoneIcon.tsx'
17
17
  import { UpdateEntry } from './UpdateEntry.tsx'
18
18
  import css from './remote.module.css'
19
19
 
20
- /** Entry props: the sidebar column state + the standard locale seat. */
21
- export type RemoteEntryProps = PropsRuntime<'sidebar.remote'> & PropsLocale<'remote'>
20
+ /** Entry props: the sidebar column state and the standard locale seat. */
21
+ export type RemoteEntryProps = PropsLocale<'remote'> & {
22
+ /** Whether the sidebar renders wide content (false = 56px rail). */
23
+ wide: boolean
24
+ }
22
25
 
23
26
  /**
24
27
  * Apply one status frame onto the current state: the ready state mirrors
@@ -49,7 +52,7 @@ function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
49
52
  * @param props - composed slot props (contract in this package).
50
53
  * @returns the entry element tree.
51
54
  */
52
- export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
55
+ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
53
56
  const [open, setOpen] = useState(false)
54
57
  const [state, setState] = useState<PanelState>({ kind: 'lan-required' })
55
58
  // Latest-state mirror for the EventSource callback: transition detection
@@ -57,17 +60,13 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
57
60
  // pure), so mint decisions read this ref instead.
58
61
  const stateRef = useRef(state)
59
62
  useEffect(() => { stateRef.current = state }, [state])
60
- const [copied, setCopied] = useState<'phone' | 'desktop' | undefined>(undefined)
63
+ const [copied, setCopied] = useState<boolean>(false)
61
64
  const eventSource = useRef<EventSource | undefined>(undefined)
62
65
  // Generation counter for the open flow: closing (or re-opening) the panel
63
66
  // bumps it, so an in-flight issue() that resolves after a close does not
64
67
  // spawn a stray EventSource.
65
68
  const openSeq = useRef(0)
66
69
 
67
- // The current workspace (the recent-workspace projection the shell's New
68
- // Session flow targets) — the deep-link target for the phone.
69
- const workspaceId = useWorkspaces(s => s.recentWorkspaceId)
70
-
71
70
  const closeEventSource = useCallback(() => {
72
71
  eventSource.current?.close()
73
72
  eventSource.current = undefined
@@ -76,7 +75,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
76
75
  const mint = useCallback(async (address?: string): Promise<PanelState> => {
77
76
  let result: IssueResponse
78
77
  try {
79
- result = await issuePair(workspaceId, address)
78
+ result = await issuePair(address)
80
79
  } catch {
81
80
  // Fetch/network failure: show an explicit state instead of silently
82
81
  // leaving the panel on its initial banner.
@@ -108,7 +107,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
108
107
  address: address ?? result.lanAddresses[0] ?? '',
109
108
  lanAddresses: result.lanAddresses,
110
109
  }
111
- }, [workspaceId])
110
+ }, [])
112
111
 
113
112
  const openPanel = useCallback(async (): Promise<void> => {
114
113
  const seq = ++openSeq.current
@@ -207,11 +206,11 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
207
206
  void mint().then(setState)
208
207
  }, [mint])
209
208
 
210
- const handleCopy = useCallback((target: 'phone' | 'desktop', url: string) => {
209
+ const handleCopy = useCallback((url: string) => {
211
210
  void copyText(url).then((ok) => {
212
211
  if (!ok) return
213
- setCopied(target)
214
- window.setTimeout(() => { setCopied(undefined) }, 1500)
212
+ setCopied(true)
213
+ window.setTimeout(() => { setCopied(false) }, 1500)
215
214
  })
216
215
  }, [])
217
216
 
@@ -12,7 +12,6 @@ import {
12
12
  import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
13
13
  import type { PairingPhase } from '../pairing.ts'
14
14
  import {
15
- desktopPairUrl,
16
15
  formatClock,
17
16
  formatLastSeen,
18
17
  type DeviceFrame,
@@ -55,11 +54,11 @@ export type PanelState =
55
54
  export interface RemotePanelProps {
56
55
  t: TranslateNS<'remote'>
57
56
  state: PanelState
58
- copied: 'phone' | 'desktop' | undefined
57
+ copied: boolean
59
58
  onClose(): void
60
59
  onStop(): void
61
60
  onRefresh(): void
62
- onCopy(target: 'phone' | 'desktop', url: string): void
61
+ onCopy(url: string): void
63
62
  /** Re-mint the QR against a different LAN address. */
64
63
  onPickAddress(address: string): void
65
64
  /** Re-mint the QR against the configured public (tunneled) base. */
@@ -147,22 +146,12 @@ export function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCo
147
146
  <div className={css.pairLinks}>
148
147
  <div className={css.pairLinkRow}>
149
148
  <div className={css.pairLinkText}>
150
- <span className={css.pairLinkLabel}>{t('pair.phoneLabel')}</span>
149
+ <span className={css.pairLinkLabel}>{t('pair.linkLabel')}</span>
151
150
  <code className={css.link} title={state.url}>{state.url}</code>
152
151
  </div>
153
- <button type="button" className={css.copyLink} onClick={() => onCopy('phone', state.url)}>
152
+ <button type="button" className={css.copyLink} onClick={() => onCopy(state.url)}>
154
153
  <IconCopyOutline16 size={14} />
155
- {copied === 'phone' ? t('action.copied') : t('action.copyPhone')}
156
- </button>
157
- </div>
158
- <div className={css.pairLinkRow}>
159
- <div className={css.pairLinkText}>
160
- <span className={css.pairLinkLabel}>{t('pair.desktopLabel')}</span>
161
- <code className={css.link} title={desktopPairUrl(state.url)}>{desktopPairUrl(state.url)}</code>
162
- </div>
163
- <button type="button" className={css.copyLink} onClick={() => onCopy('desktop', desktopPairUrl(state.url))}>
164
- <IconCopyOutline16 size={14} />
165
- {copied === 'desktop' ? t('action.copied') : t('action.copyDesktop')}
154
+ {copied ? t('action.copied') : t('action.copyLink')}
166
155
  </button>
167
156
  </div>
168
157
  </div>
@@ -4,10 +4,14 @@
4
4
  * renders, bound to the `remote-web-ui` settings namespace.
5
5
  */
6
6
 
7
+ import { useEffect, useState } from 'react'
7
8
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
8
- import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
9
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
10
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
9
11
  import { PluginSettingsCard, ValueField, BooleanField } from './PluginSettingsCard.tsx'
10
12
  import { CardForm, booleanField, numberField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
13
+ import { readLanBindStatus, type LanBindFrame } from './pair-api.ts'
14
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
11
15
 
12
16
  /** The remote-control fields this card edits (the namespace's full schema). */
13
17
  export interface RemoteSettings {
@@ -29,8 +33,11 @@ export interface RemoteSettings {
29
33
  publicBaseUrl?: string
30
34
  /** When on, the plugin runs its own Cloudflare quick tunnel automatically. */
31
35
  autoTunnel?: boolean
32
- /** Mobile composer: plain Enter sends; off means Enter inserts a newline. */
33
- mobileEnterToSend?: boolean
36
+ /**
37
+ * LAN bind toggle: once flipped, the plugin manages the profile patch's
38
+ * webserver block (0.0.0.0 / 127.0.0.1) and the host firewall rule.
39
+ */
40
+ lanBind?: boolean
34
41
  }
35
42
 
36
43
  /** What the remote-control card renders. */
@@ -53,8 +60,8 @@ export interface RemoteSettingsCardState extends CardShell {
53
60
  publicBaseUrl: CardFieldState
54
61
  /** Auto public tunnel switch. */
55
62
  autoTunnel: CardFieldState
56
- /** Mobile composer Enter-to-send switch. */
57
- mobileEnterToSend: CardFieldState
63
+ /** LAN bind toggle. */
64
+ lanBind: CardFieldState
58
65
  }
59
66
 
60
67
  /** The registration-side face the card's slot entry injects. */
@@ -82,7 +89,7 @@ export class RemoteSettingsCardController {
82
89
  booleanField('requirePairingForLan'),
83
90
  textField('publicBaseUrl'),
84
91
  booleanField('autoTunnel'),
85
- booleanField('mobileEnterToSend'),
92
+ booleanField('lanBind'),
86
93
  ])
87
94
  this.store = this.form.bind(() => this.projection())
88
95
  }
@@ -99,7 +106,7 @@ export class RemoteSettingsCardController {
99
106
  requirePairingForLan: this.form.field('requirePairingForLan'),
100
107
  publicBaseUrl: this.form.field('publicBaseUrl'),
101
108
  autoTunnel: this.form.field('autoTunnel'),
102
- mobileEnterToSend: this.form.field('mobileEnterToSend'),
109
+ lanBind: this.form.field('lanBind'),
103
110
  }
104
111
  }
105
112
 
@@ -246,18 +253,70 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
246
253
  onEdit={(text) => { props.edit('autoTunnel', text) }}
247
254
  onReset={() => { props.resetField('autoTunnel') }}
248
255
  />
256
+ <LanBindStatus t={t} />
249
257
  <BooleanField
250
- id="settings-remote-mobile-enter"
251
- label={t('settings.mobileEnterToSend')}
252
- hint={t('settings.mobileEnterToSendHint')}
258
+ id="settings-remote-lan-bind"
259
+ label={t('settings.lanBind')}
260
+ hint={t('settings.lanBindHint')}
253
261
  inheritLabel={t('settings.inherit')}
254
262
  onLabel={t('settings.on')}
255
263
  offLabel={t('settings.off')}
256
264
  {...fieldProps}
257
- {...state.mobileEnterToSend}
258
- onEdit={(text) => { props.edit('mobileEnterToSend', text) }}
259
- onReset={() => { props.resetField('mobileEnterToSend') }}
265
+ {...state.lanBind}
266
+ onEdit={(text) => { props.edit('lanBind', text) }}
267
+ onReset={() => { props.resetField('lanBind') }}
260
268
  />
261
269
  </PluginSettingsCard>
262
270
  )
263
271
  }
272
+
273
+ /**
274
+ * Live LAN-bind facts above the toggle: the managed block's host, the live
275
+ * bind, the firewall summary, and the reachable LAN URLs. Polls the
276
+ * loopback-only status endpoint; a failure (non-loopback origin) renders
277
+ * nothing — the pairing panel carries the loopback banner instead.
278
+ */
279
+ function LanBindStatus({ t }: { t: TranslateNS<'remote'> }) {
280
+ const [frame, setFrame] = useState<LanBindFrame | undefined>(undefined)
281
+ useEffect(() => {
282
+ let alive = true
283
+ const read = (): void => {
284
+ void readLanBindStatus().then((value) => {
285
+ if (alive) setFrame(value)
286
+ }).catch(() => {})
287
+ }
288
+ read()
289
+ const timer = window.setInterval(read, 10_000)
290
+ return () => {
291
+ alive = false
292
+ window.clearInterval(timer)
293
+ }
294
+ }, [])
295
+ if (frame === undefined) return null
296
+ const lanOn = frame.blockHost === '0.0.0.0'
297
+ const firewallText = frame.firewall.managed
298
+ ? t(frame.firewall.ok ? 'lan.firewall.ok' : 'lan.firewall.bad')
299
+ : t('lan.firewall.unmanaged')
300
+ const lines: string[] = [
301
+ t('lan.bind', { host: frame.bindHost, port: String(frame.port) }) + ' · ' + firewallText,
302
+ ]
303
+ if (lanOn && frame.lanUrls.length > 0) {
304
+ lines.push(t('lan.urls', { urls: frame.lanUrls.join(' ') }))
305
+ }
306
+ if (!lanOn) {
307
+ lines.push(t('lan.off'))
308
+ }
309
+ if (frame.setting === null) {
310
+ lines.push(t('lan.untouched'))
311
+ }
312
+ if (frame.pendingRestart === true) {
313
+ lines.push(t('lan.pendingRestart'))
314
+ }
315
+ return (
316
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 12, lineHeight: '18px', opacity: 0.85 }}>
317
+ {lines.map((line, index) => (
318
+ <div key={index} style={{ wordBreak: 'break-all' }}>{line}</div>
319
+ ))}
320
+ </div>
321
+ )
322
+ }
@@ -1,25 +1,19 @@
1
1
  /**
2
- * Phone-side boot flow: the QR link's `pair` + `workspace` parameters.
3
- * Runs from the client apply on every page load, on any device:
4
- * - `pair` present → accept the token, then reload so the whole SPA boots
5
- * with the device cookie (the accept endpoint is exempt from the pairing
6
- * gate; every other /api request needs the cookie).
7
- * - `workspace` present (and paired) → deep-link: connect that workspace's
8
- * session and open it, then strip the parameter.
9
- * Failure of accept leaves a sessionStorage marker the entry renders as a
10
- * one-time notice.
2
+ * Pair boot flow: the QR link's `pair` parameter. Runs from the client
3
+ * apply on every page load, on any device: `pair` present → accept the
4
+ * token, then reload so the whole SPA boots with the device cookie (the
5
+ * accept endpoint is exempt from the pairing gate; every other /api request
6
+ * needs the cookie). Every device — phone or PC — lands on the same official
7
+ * Web GUI; phones get the injected portrait adaptation. Failure of accept
8
+ * leaves a sessionStorage marker the entry renders as a one-time notice.
11
9
  */
12
10
 
13
11
  import type { Context } from '@deepseek-ai/cordis'
14
- import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
15
12
  import { acceptPair, readPairParams } from './pair-api.ts'
16
13
 
17
14
  /** sessionStorage key for the failed-pair notice. */
18
15
  export const PAIR_FAILED_MARKER = 'dsh-remote-pair-failed'
19
16
 
20
- /** Poll budget for the runtime services (activation order is unconstrained). */
21
- const SERVICE_WAIT_MS = 10_000
22
-
23
17
  /**
24
18
  * The page-navigation surface the boot flow drives. Browser pages use the
25
19
  * default (window.location/history); tests inject a fake.
@@ -51,30 +45,22 @@ export const browserPage: PageSurface = {
51
45
  },
52
46
  }
53
47
 
54
- /** Whether this browser looks like a phone/tablet (the simplified mobile surface). */
55
- export function isMobileSurface(): boolean {
56
- if (typeof navigator === 'undefined') return false
57
- return /Android|iPhone|iPad|iPod|Mobile|mobile/i.test(navigator.userAgent)
58
- }
59
-
60
48
  /**
61
- * Run the pair/workspace boot flow for this page load.
62
- * @param ctx - client root context (workspaces/sessions read at need time).
49
+ * Run the pair boot flow for this page load.
50
+ * @param ctx - client root context (unused since the deep-link retirement;
51
+ * kept for call-site stability).
63
52
  * @param search - the current location.search.
64
53
  * @param page - the page surface (defaults to the browser).
65
54
  */
66
55
  export function runPairBootFlow(ctx: Context, search: string, page: PageSurface = browserPage): void {
56
+ void ctx
67
57
  const params = readPairParams(search)
68
58
  if (params.pair !== undefined) {
69
59
  void runAccept(params.pair, page)
70
- return
71
- }
72
- if (params.workspace !== undefined) {
73
- void runDeepLink(ctx, params.workspace, page)
74
60
  }
75
61
  }
76
62
 
77
- /** Accept the token, then enter the matching desktop/mobile surface. */
63
+ /** Accept the token, then reload into the paired official UI. */
78
64
  async function runAccept(token: string, page: PageSurface): Promise<void> {
79
65
  let ok = false
80
66
  try {
@@ -90,54 +76,7 @@ async function runAccept(token: string, page: PageSurface): Promise<void> {
90
76
  url.searchParams.delete('pair')
91
77
  page.replaceState(`${url.pathname}${url.search}${url.hash}`)
92
78
  if (ok) {
93
- // Phones land on the standalone simplified surface (the full desktop UI
94
- // is not built for small screens). Keep the workspace target so the
95
- // mobile surface can open the intended workspace instead of losing the
96
- // QR context at this navigation boundary.
97
- if (isMobileSurface()) {
98
- url.pathname = '/m/'
99
- page.navigate(`${url.pathname}${url.search}${url.hash}`)
100
- } else {
101
- page.reload()
102
- }
79
+ // Every device reloads into the same official Web GUI at this origin.
80
+ page.reload()
103
81
  }
104
82
  }
105
-
106
- /**
107
- * Connect the deep-linked workspace and open its session. Waits for the
108
- * runtime services AND for the target workspace to appear in the workspace
109
- * list (both are asynchronous after boot), then connects and opens; gives
110
- * up silently within the budget — the workspace param is stripped either
111
- * way, so a late failure cannot loop.
112
- * @param ctx - client root context.
113
- * @param workspaceId - the target workspace.
114
- * @param page - the page surface.
115
- */
116
- async function runDeepLink(ctx: Context, workspaceId: string, page: PageSurface): Promise<void> {
117
- const target = workspaceId as WorkspaceId
118
- const deadline = Date.now() + SERVICE_WAIT_MS
119
- while (Date.now() < deadline) {
120
- const workspaces = ctx.get('workspaces')
121
- const sessions = ctx.get('sessions') as { open(id: string): void } | undefined
122
- if (workspaces !== undefined && sessions !== undefined) {
123
- const items = workspaces.list.getSnapshot().items
124
- if (items.some(item => item.workspaceId === target)) {
125
- try {
126
- // Open unconditionally: a host-side "current" session may already
127
- // exist (multi-client mirroring), but the QR's workspace target is
128
- // explicit user intent and must win.
129
- const sessionId = await workspaces.connectWorkspace(target)
130
- sessions.open(sessionId)
131
- } catch {
132
- // Unknown workspace or a failed connect: fall through to the
133
- // runtime's own default initial selection.
134
- }
135
- break
136
- }
137
- }
138
- await new Promise(resolve => setTimeout(resolve, 200))
139
- }
140
- const url = new URL(page.href)
141
- url.searchParams.delete('workspace')
142
- page.replaceState(`${url.pathname}${url.search}${url.hash}`)
143
- }
@@ -1,21 +1,25 @@
1
1
  /**
2
- * Mobile remote control — browser half. Registers the `remote` dictionaries,
3
- * the sidebar-foot entry (phone trigger + pairing panel) into the
4
- * ui-sidebar-declared `sidebar.remote` seat, and runs the phone-side boot
5
- * flow (pair accept + workspace deep-link + presence heartbeats) plus the
6
- * one-time failed-pair notice. Export discipline: packages/client/AGENTS.md
7
- * — the /client surface carries only what cordis loading needs plus types.
2
+ * Remote control — browser half. Registers the `remote` dictionaries, the
3
+ * sidebar-foot entry (phone trigger + pairing panel + update trigger), and
4
+ * the pair boot flow (accept + presence heartbeats) plus the one-time
5
+ * failed-pair notice. The portrait-touch adaptation of the official UI
6
+ * starts at module scope (startMobileAdapt) so its focus guard is installed
7
+ * before the app boots. Export discipline: packages/client/AGENTS.md — the
8
+ * /client surface carries only what cordis loading needs plus types.
8
9
  */
9
10
  import { createElement } from 'react'
10
11
  import { createRoot } from 'react-dom/client'
11
- import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'
12
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
13
+ import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client'
12
14
  // Type-only: pulls the locale plugin's Context merge (ctx.locale) and the
13
- // ui-sidebar SlotMap merge (the 'sidebar.remote' hole).
15
+ // ui-sidebar SlotMap merge (the 'sidebar.footer.action' hole).
14
16
  import type {} from '@deepseek-ai/dsh-client-locale/client'
15
17
  // Type-only: pulls the settings-surface SlotMap merge (the 'settings.section'
16
18
  // entry) and the ctx.settingsScope Context merge.
17
19
  import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
18
20
  import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
21
+ // Type-only: pulls the ctx.slots merge (the renderer owns the slot registry since 0.1.2).
22
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
19
23
  import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
20
24
  import { FooterRemoteEntry } from './FooterRemoteEntry.tsx'
21
25
  import { RemoteEntry } from './RemoteEntry.tsx'
@@ -34,6 +38,13 @@ import {
34
38
  } from './remote-channel.ts'
35
39
  import { FenceNotice } from './FenceNotice.tsx'
36
40
  import { reportDailyHeartbeat } from './telemetry.ts'
41
+ import { startMobileAdapt, type RemoteAdaptGlobal } from './mobile-adapt.ts'
42
+
43
+ // Portrait-touch adaptation of the official UI: installed at module scope so
44
+ // the composer focus guard exists before any app entry mounts React. The
45
+ // layer self-evaluates and reverts with the viewport; the plugin apply below
46
+ // wires its toggleSidebar onto the official layout service.
47
+ startMobileAdapt()
37
48
 
38
49
  export type { RemoteEntryProps } from './RemoteEntry.tsx'
39
50
  export type { PanelState, RemotePanelProps } from './RemotePanel.tsx'
@@ -55,7 +66,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
55
66
  * sidebar shell on deployments that carry the feature seat; the shell
56
67
  * passes only its column display state.
57
68
  */
58
- 'sidebar.remote': { kind: 'single'; scope: 'root'; owner: SidebarRemoteOwnerProps }
59
69
  /**
60
70
  * The child slot the Web UI plugin group declares; this card registers
61
71
  * into the group instead of the top-level `settings.plugin.item` list.
@@ -119,7 +129,48 @@ export function apply(ctx: ClientContext): void {
119
129
  }
120
130
  }, 'remote-web-ui: dictionaries')
121
131
 
132
+ // The mobile adapt's whale button expands the collapsed sidebar and the
133
+ // activation closes the details panel — both through the official layout
134
+ // service (ctx.layout.toggleSidebar / closeDetails flip the panel state;
135
+ // the narrow-viewport semantics open the drawer).
136
+ const layout = ctx.get('layout') as { toggleSidebar?: () => void; closeDetails?: () => void } | undefined
137
+ const adapt = (window as unknown as { __dshRemoteAdapt?: RemoteAdaptGlobal }).__dshRemoteAdapt
138
+ // The official layout face throws (by contract) when the root entry has
139
+ // not mounted yet; these closures also fire from gestures racing that
140
+ // first mount, so they tolerate the throw instead of surfacing it.
141
+ const layoutCall = (call: (() => void) | undefined): void => {
142
+ try {
143
+ call?.()
144
+ } catch {
145
+ // Boot-order throw: the panel action is a no-op before the root entry.
146
+ }
147
+ }
148
+ if (layout !== undefined && adapt !== undefined) {
149
+ if (typeof layout.toggleSidebar === 'function') {
150
+ adapt.toggleSidebar = () => {
151
+ layoutCall(layout.toggleSidebar)
152
+ }
153
+ }
154
+ if (typeof layout.closeDetails === 'function') {
155
+ adapt.closeDetails = () => {
156
+ layoutCall(layout.closeDetails)
157
+ }
158
+ }
159
+ }
160
+ if (adapt !== undefined && adapt.toggleSidebar === null) {
161
+ // Layout face unavailable (older composition): fall back to clicking the
162
+ // official rail toggle when it exists.
163
+ adapt.toggleSidebar = () => {
164
+ (document.querySelector('[class$="_railFish"] button, [class$="_logoRow"] [class*="_iconButton"]') as HTMLElement | null)?.click()
165
+ }
166
+ }
167
+
122
168
  const t = ctx.locale.bind(NS)
169
+ // Hand the `remote` translate seat to the module-scope adaptation layer:
170
+ // the whale/compact-picker labels were rendered with the English fallback
171
+ // before any dictionary existed; the wiring plus the layer's own sync tick
172
+ // re-render them in the active locale.
173
+ if (adapt !== undefined) adapt.translate = t
123
174
  const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
124
175
  const settingsScope = binder.bind<RemoteSettings>({ namespace: REMOTE_WEB_UI_NS })
125
176
  const enabled = (): boolean => {
@@ -129,37 +180,29 @@ export function apply(ctx: ClientContext): void {
129
180
  : snapshot.status === 'unavailable'
130
181
  }
131
182
 
132
- // Sidebar foot entry: the shell declares 'sidebar.remote' in unconstrained
133
- // order, so registration is declaration-aware — slots.inject waits on the
134
- // declaration, removes the contribution when it collapses, and re-runs
135
- // after a redeclaration. The entry follows the plugin's enabled setting:
136
- // toggling it off removes the trigger, toggling it back on re-registers it.
137
- ctx.slots.inject('sidebar.remote', () => {
138
- let disposeEntry: (() => void) | undefined
139
- const syncEntry = (): void => {
140
- if (enabled() && disposeEntry === undefined) {
141
- try {
142
- disposeEntry = ctx.slots.register({ name: 'sidebar.remote', locale: NS }, RemoteEntry)
143
- } catch {
144
- // ignore registration collision
145
- }
146
- } else if (!enabled() && disposeEntry !== undefined) {
147
- disposeEntry()
148
- disposeEntry = undefined
149
- }
150
- }
151
- const unsubscribe = settingsScope.subscribe(syncEntry)
152
- syncEntry()
153
- return () => {
154
- unsubscribe()
155
- disposeEntry?.()
156
- }
157
- })
183
+ // Master switch for the adaptation layer: the module-scope install runs
184
+ // before any config is readable, so flip it once the settings snapshot is
185
+ // bound and on every later change (disabled plugin = no injected surface).
186
+ // Also replay the pending closeDetails: the first portrait apply ran
187
+ // before the layout face was wired, so its closeDetails was a no-op and a
188
+ // restored details panel would otherwise sit hidden until landscape.
189
+ const syncAdaptEnabled = (): void => {
190
+ ;(window as unknown as { __dshRemoteAdapt?: RemoteAdaptGlobal }).__dshRemoteAdapt?.setEnabled?.(enabled())
191
+ }
192
+ settingsScope.subscribe(syncAdaptEnabled)
193
+ syncAdaptEnabled()
194
+ // The replay closes a details panel restored before the wiring; the
195
+ // layout face throws while the root entry has not mounted yet (a real
196
+ // boot-order race on slow remote loads), and the plugin's apply world
197
+ // must survive it — the replay is a best-effort no-op then.
198
+ try {
199
+ adapt?.flushCloseDetails?.()
200
+ } catch {}
158
201
 
159
- // Current shells declare `sidebar.footer.action` instead of the legacy
160
- // `sidebar.remote` seat; this fallback registers the same entry there when
161
- // the legacy seat never arrives (declaration-aware: only one of the two
162
- // injects ever fires, so the trigger can never render twice).
202
+ // Sidebar foot entry: the sidebar foot seat is `sidebar.footer.action` in
203
+ // the 0.1.2 shell composition (the legacy `sidebar.remote` seat is gone
204
+ // upstream). The pairing link is origin-agnostic, so the entry needs no
205
+ // workspace source.
163
206
  ctx.slots.inject('sidebar.footer.action', () => {
164
207
  let disposeEntry: (() => void) | undefined
165
208
  const syncEntry = (): void => {