@linxin666/dsh-remote-web-ui 0.1.12 → 0.1.13
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 +6 -0
- package/README.md +12 -1
- package/README.zh.md +171 -0
- package/lib/client.js +169 -98
- package/lib/client.js.map +1 -1
- package/lib/index.js +224 -4
- package/lib/mobile.js +109 -33
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -1
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +10 -2
- package/lib/types/client/deep-link.js +10 -5
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +8 -0
- package/lib/types/client/update-api.d.ts +10 -0
- package/lib/types/client/update-api.d.ts.map +1 -1
- package/lib/types/client/update-api.js +22 -2
- package/lib/types/index.d.ts +7 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +7 -1
- package/lib/types/mobile/api.d.ts +9 -2
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +7 -3
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +3 -0
- package/lib/types/mobile/views/App.d.ts +3 -1
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +18 -3
- package/lib/types/mobile/views/ChatView.d.ts +6 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +95 -7
- package/lib/types/mobile/views/ChatView.test.js +161 -3
- package/lib/types/mobile/views/WorkspaceView.d.ts +3 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +9 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +2 -0
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/WorkspaceView.test.js +56 -0
- package/lib/types/mobile-api.d.ts +2 -0
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +17 -2
- package/package.json +1 -11
- package/src/client/PluginSettingsCard.tsx +30 -5
- package/src/client/RemoteSettingsCard.tsx +18 -0
- package/src/client/UpdateEntry.tsx +10 -2
- package/src/client/deep-link.ts +10 -4
- package/src/client/locales.ts +8 -0
- package/src/client/settings-card.module.css +139 -144
- package/src/client/update-api.ts +22 -2
- package/src/index.ts +14 -1
- package/src/mobile/api.ts +14 -2
- package/src/mobile/mobile-styles.ts +3 -0
- package/src/mobile/views/App.tsx +22 -3
- package/src/mobile/views/ChatView.test.tsx +185 -3
- package/src/mobile/views/ChatView.tsx +102 -8
- package/src/mobile/views/WorkspaceView.test.tsx +66 -0
- package/src/mobile/views/WorkspaceView.tsx +11 -2
- package/src/mobile-api.ts +20 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/** Mobile workspace landing: roster rendering and QR deep-link selection. */
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
5
|
+
import type { WorkspaceView as WorkspaceRow } from '@deepseek-ai/dsh-host-apiproxy/api/workspace'
|
|
6
|
+
import { mobileWorkspaceTarget } from './App.tsx'
|
|
7
|
+
import { WorkspaceView } from './WorkspaceView.tsx'
|
|
8
|
+
|
|
9
|
+
vi.mock('../api.ts', () => ({
|
|
10
|
+
listWorkspaces: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
import { listWorkspaces } from '../api.ts'
|
|
13
|
+
|
|
14
|
+
const listWorkspacesMock = vi.mocked(listWorkspaces)
|
|
15
|
+
const workspaces: WorkspaceRow[] = [
|
|
16
|
+
{
|
|
17
|
+
workspaceId: 'ws-1' as never,
|
|
18
|
+
path: '/tmp/first',
|
|
19
|
+
title: 'First',
|
|
20
|
+
sessionIds: [] as never,
|
|
21
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
22
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
workspaceId: 'ws-2' as never,
|
|
26
|
+
path: '/tmp/second',
|
|
27
|
+
title: 'Second',
|
|
28
|
+
sessionIds: [] as never,
|
|
29
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
30
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup()
|
|
36
|
+
vi.clearAllMocks()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('mobile workspace deep link', () => {
|
|
40
|
+
it('reads a non-empty workspace target from the query', () => {
|
|
41
|
+
expect(mobileWorkspaceTarget('?workspace=ws-2')).toBe('ws-2')
|
|
42
|
+
expect(mobileWorkspaceTarget('?workspace=')).toBeUndefined()
|
|
43
|
+
expect(mobileWorkspaceTarget('')).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('opens the targeted workspace as soon as the roster loads', async () => {
|
|
47
|
+
listWorkspacesMock.mockResolvedValue(workspaces)
|
|
48
|
+
const onPick = vi.fn()
|
|
49
|
+
|
|
50
|
+
render(<WorkspaceView initialWorkspaceId="ws-2" onPick={onPick} />)
|
|
51
|
+
|
|
52
|
+
await waitFor(() => expect(onPick).toHaveBeenCalledWith(workspaces[1]))
|
|
53
|
+
expect(onPick).toHaveBeenCalledTimes(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('falls back to the roster when the target no longer exists', async () => {
|
|
57
|
+
listWorkspacesMock.mockResolvedValue(workspaces)
|
|
58
|
+
const onPick = vi.fn()
|
|
59
|
+
|
|
60
|
+
render(<WorkspaceView initialWorkspaceId="missing" onPick={onPick} />)
|
|
61
|
+
|
|
62
|
+
expect(await screen.findByText('First')).toBeTruthy()
|
|
63
|
+
expect(await screen.findByText('Second')).toBeTruthy()
|
|
64
|
+
expect(onPick).not.toHaveBeenCalled()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -13,6 +13,8 @@ import { ThemeToggle } from '../theme-toggle.tsx'
|
|
|
13
13
|
|
|
14
14
|
/** Props for the workspace roster. */
|
|
15
15
|
export interface WorkspaceViewProps {
|
|
16
|
+
/** Workspace carried by the pairing link; opened after the roster loads. */
|
|
17
|
+
initialWorkspaceId?: string
|
|
16
18
|
/** Open one workspace's session list. */
|
|
17
19
|
onPick(workspace: WorkspaceRow): void
|
|
18
20
|
}
|
|
@@ -22,7 +24,7 @@ export interface WorkspaceViewProps {
|
|
|
22
24
|
* @param props - the pick action.
|
|
23
25
|
* @returns the roster.
|
|
24
26
|
*/
|
|
25
|
-
export function WorkspaceView({ onPick }: WorkspaceViewProps) {
|
|
27
|
+
export function WorkspaceView({ initialWorkspaceId, onPick }: WorkspaceViewProps) {
|
|
26
28
|
const [items, setItems] = useState<WorkspaceRow[] | undefined>(undefined)
|
|
27
29
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
28
30
|
|
|
@@ -31,6 +33,13 @@ export function WorkspaceView({ onPick }: WorkspaceViewProps) {
|
|
|
31
33
|
void listWorkspaces().then(
|
|
32
34
|
(rows) => {
|
|
33
35
|
if (cancelled) return
|
|
36
|
+
const target = initialWorkspaceId === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: rows.find(workspace => workspace.workspaceId === initialWorkspaceId)
|
|
39
|
+
if (target !== undefined) {
|
|
40
|
+
onPick(target)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
34
43
|
setItems(rows)
|
|
35
44
|
},
|
|
36
45
|
(reason: unknown) => {
|
|
@@ -39,7 +48,7 @@ export function WorkspaceView({ onPick }: WorkspaceViewProps) {
|
|
|
39
48
|
},
|
|
40
49
|
)
|
|
41
50
|
return () => { cancelled = true }
|
|
42
|
-
}, [])
|
|
51
|
+
}, [initialWorkspaceId, onPick])
|
|
43
52
|
|
|
44
53
|
if (error !== undefined) {
|
|
45
54
|
return (
|
package/src/mobile-api.ts
CHANGED
|
@@ -40,6 +40,13 @@ const MOBILE_ALLOWLIST = new Set([
|
|
|
40
40
|
'session.rename',
|
|
41
41
|
])
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Locally answered display-preference method (the phone's read-only
|
|
45
|
+
* surface preferences; never proxied to the host ApiProxy and never a
|
|
46
|
+
* settings-domain write).
|
|
47
|
+
*/
|
|
48
|
+
const MOBILE_PREFERENCES_METHOD = 'mobile.preferences'
|
|
49
|
+
|
|
43
50
|
/** One session.list page (thin phones load incrementally). */
|
|
44
51
|
const SESSION_PAGE_SIZE = 20
|
|
45
52
|
|
|
@@ -69,6 +76,8 @@ export interface MobileApiDeps {
|
|
|
69
76
|
service: PairingService
|
|
70
77
|
/** The host ApiProxy service (injected by the plugin). */
|
|
71
78
|
apiProxy: ApiProxy
|
|
79
|
+
/** The resolved mobile composer preference (live per request). */
|
|
80
|
+
mobileEnterToSend: () => boolean
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/** Mobile API route paths. */
|
|
@@ -87,7 +96,7 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`
|
|
|
87
96
|
* @returns the routes to register on webServer.
|
|
88
97
|
*/
|
|
89
98
|
export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
90
|
-
const { service, apiProxy } = deps
|
|
99
|
+
const { service, apiProxy, mobileEnterToSend } = deps
|
|
91
100
|
|
|
92
101
|
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
93
102
|
const gateOk = (req: IncomingMessage): boolean => {
|
|
@@ -116,7 +125,8 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
116
125
|
return
|
|
117
126
|
}
|
|
118
127
|
const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length)
|
|
119
|
-
|
|
128
|
+
const local = method === MOBILE_PREFERENCES_METHOD
|
|
129
|
+
if (!MOBILE_ALLOWLIST.has(method) && !local) {
|
|
120
130
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: `method ${method} is not exposed to the mobile surface` } })
|
|
121
131
|
return
|
|
122
132
|
}
|
|
@@ -133,6 +143,14 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
133
143
|
writeJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'missing rpcId' } })
|
|
134
144
|
return
|
|
135
145
|
}
|
|
146
|
+
if (local) {
|
|
147
|
+
writeJson(res, 200, {
|
|
148
|
+
type: 'server-response',
|
|
149
|
+
rpcId,
|
|
150
|
+
result: { ok: true, value: { mobileEnterToSend: mobileEnterToSend() } },
|
|
151
|
+
})
|
|
152
|
+
return
|
|
153
|
+
}
|
|
136
154
|
try {
|
|
137
155
|
const response = await dispatch(apiProxy, method, parsed?.payload, rpcId)
|
|
138
156
|
writeJson(res, 200, response)
|