@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.
Files changed (64) hide show
  1. package/README.i18n.yaml +6 -0
  2. package/README.md +12 -1
  3. package/README.zh.md +171 -0
  4. package/lib/client.js +169 -98
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +224 -4
  7. package/lib/mobile.js +109 -33
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  10. package/lib/types/client/PluginSettingsCard.js +3 -2
  11. package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
  12. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  13. package/lib/types/client/RemoteSettingsCard.js +3 -1
  14. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  15. package/lib/types/client/UpdateEntry.js +10 -2
  16. package/lib/types/client/deep-link.js +10 -5
  17. package/lib/types/client/locales.d.ts +8 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales.js +8 -0
  20. package/lib/types/client/update-api.d.ts +10 -0
  21. package/lib/types/client/update-api.d.ts.map +1 -1
  22. package/lib/types/client/update-api.js +22 -2
  23. package/lib/types/index.d.ts +7 -0
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/lib/types/index.js +7 -1
  26. package/lib/types/mobile/api.d.ts +9 -2
  27. package/lib/types/mobile/api.d.ts.map +1 -1
  28. package/lib/types/mobile/api.js +7 -3
  29. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  30. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  31. package/lib/types/mobile/mobile-styles.js +3 -0
  32. package/lib/types/mobile/views/App.d.ts +3 -1
  33. package/lib/types/mobile/views/App.d.ts.map +1 -1
  34. package/lib/types/mobile/views/App.js +18 -3
  35. package/lib/types/mobile/views/ChatView.d.ts +6 -0
  36. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  37. package/lib/types/mobile/views/ChatView.js +95 -7
  38. package/lib/types/mobile/views/ChatView.test.js +161 -3
  39. package/lib/types/mobile/views/WorkspaceView.d.ts +3 -1
  40. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  41. package/lib/types/mobile/views/WorkspaceView.js +9 -2
  42. package/lib/types/mobile/views/WorkspaceView.test.d.ts +2 -0
  43. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +1 -0
  44. package/lib/types/mobile/views/WorkspaceView.test.js +56 -0
  45. package/lib/types/mobile-api.d.ts +2 -0
  46. package/lib/types/mobile-api.d.ts.map +1 -1
  47. package/lib/types/mobile-api.js +17 -2
  48. package/package.json +1 -11
  49. package/src/client/PluginSettingsCard.tsx +30 -5
  50. package/src/client/RemoteSettingsCard.tsx +18 -0
  51. package/src/client/UpdateEntry.tsx +10 -2
  52. package/src/client/deep-link.ts +10 -4
  53. package/src/client/locales.ts +8 -0
  54. package/src/client/settings-card.module.css +139 -144
  55. package/src/client/update-api.ts +22 -2
  56. package/src/index.ts +14 -1
  57. package/src/mobile/api.ts +14 -2
  58. package/src/mobile/mobile-styles.ts +3 -0
  59. package/src/mobile/views/App.tsx +22 -3
  60. package/src/mobile/views/ChatView.test.tsx +185 -3
  61. package/src/mobile/views/ChatView.tsx +102 -8
  62. package/src/mobile/views/WorkspaceView.test.tsx +66 -0
  63. package/src/mobile/views/WorkspaceView.tsx +11 -2
  64. 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
- if (!MOBILE_ALLOWLIST.has(method)) {
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)