@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.
- package/README.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +993 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +79 -39
- package/lib/types/client/locales.d.ts +32 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +42 -22
- package/lib/types/client/mobile-adapt.d.ts +49 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +1002 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +82 -39
- package/src/client/locales.ts +42 -22
- package/src/client/mobile-adapt.ts +954 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17658
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -149
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -561
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -63
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -821
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -685
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -268
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1342
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- 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
|
|
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
|
|
21
|
-
export type RemoteEntryProps =
|
|
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,
|
|
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<
|
|
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(
|
|
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
|
-
}, [
|
|
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((
|
|
209
|
+
const handleCopy = useCallback((url: string) => {
|
|
211
210
|
void copyText(url).then((ok) => {
|
|
212
211
|
if (!ok) return
|
|
213
|
-
setCopied(
|
|
214
|
-
window.setTimeout(() => { setCopied(
|
|
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:
|
|
57
|
+
copied: boolean
|
|
59
58
|
onClose(): void
|
|
60
59
|
onStop(): void
|
|
61
60
|
onRefresh(): void
|
|
62
|
-
onCopy(
|
|
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.
|
|
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(
|
|
152
|
+
<button type="button" className={css.copyLink} onClick={() => onCopy(state.url)}>
|
|
154
153
|
<IconCopyOutline16 size={14} />
|
|
155
|
-
{copied
|
|
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
|
|
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
|
-
/**
|
|
33
|
-
|
|
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
|
-
/**
|
|
57
|
-
|
|
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('
|
|
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
|
-
|
|
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-
|
|
251
|
-
label={t('settings.
|
|
252
|
-
hint={t('settings.
|
|
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.
|
|
258
|
-
onEdit={(text) => { props.edit('
|
|
259
|
-
onReset={() => { props.resetField('
|
|
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
|
+
}
|
package/src/client/deep-link.ts
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
62
|
-
* @param ctx - client root context (
|
|
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
|
|
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
|
-
//
|
|
94
|
-
|
|
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
|
-
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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.
|
|
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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
//
|
|
160
|
-
// `sidebar.remote` seat
|
|
161
|
-
//
|
|
162
|
-
//
|
|
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 => {
|