@linxin666/dsh-remote-web-ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +346 -0
  3. package/cordis.patch.yml +10 -0
  4. package/lib/client.js +2817 -0
  5. package/lib/index.js +1455 -0
  6. package/lib/invariant.js +21 -0
  7. package/lib/types/client/PairFailedNotice.d.ts +12 -0
  8. package/lib/types/client/PairFailedNotice.d.ts.map +1 -0
  9. package/lib/types/client/PhoneIcon.d.ts +19 -0
  10. package/lib/types/client/PhoneIcon.d.ts.map +1 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts +78 -0
  12. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  13. package/lib/types/client/RemoteEntry.d.ts +10 -0
  14. package/lib/types/client/RemoteEntry.d.ts.map +1 -0
  15. package/lib/types/client/RemotePanel.d.ts +50 -0
  16. package/lib/types/client/RemotePanel.d.ts.map +1 -0
  17. package/lib/types/client/RemoteSettingsCard.d.ts +75 -0
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -0
  19. package/lib/types/client/deep-link.d.ts +40 -0
  20. package/lib/types/client/deep-link.d.ts.map +1 -0
  21. package/lib/types/client/index.d.ts +54 -0
  22. package/lib/types/client/index.d.ts.map +1 -0
  23. package/lib/types/client/locales.d.ts +144 -0
  24. package/lib/types/client/locales.d.ts.map +1 -0
  25. package/lib/types/client/pair-api.d.ts +100 -0
  26. package/lib/types/client/pair-api.d.ts.map +1 -0
  27. package/lib/types/client/settings-form.d.ts +119 -0
  28. package/lib/types/client/settings-form.d.ts.map +1 -0
  29. package/lib/types/gate.d.ts +58 -0
  30. package/lib/types/gate.d.ts.map +1 -0
  31. package/lib/types/index.d.ts +78 -0
  32. package/lib/types/index.d.ts.map +1 -0
  33. package/lib/types/invariant.d.ts +16 -0
  34. package/lib/types/invariant.d.ts.map +1 -0
  35. package/lib/types/lan.d.ts +15 -0
  36. package/lib/types/lan.d.ts.map +1 -0
  37. package/lib/types/mobile/api.d.ts +64 -0
  38. package/lib/types/mobile/api.d.ts.map +1 -0
  39. package/lib/types/mobile/index.d.ts +10 -0
  40. package/lib/types/mobile/index.d.ts.map +1 -0
  41. package/lib/types/mobile/messages.d.ts +103 -0
  42. package/lib/types/mobile/messages.d.ts.map +1 -0
  43. package/lib/types/mobile/messages.test.d.ts +2 -0
  44. package/lib/types/mobile/messages.test.d.ts.map +1 -0
  45. package/lib/types/mobile/mobile-styles.d.ts +6 -0
  46. package/lib/types/mobile/mobile-styles.d.ts.map +1 -0
  47. package/lib/types/mobile/mobile-theme.d.ts +22 -0
  48. package/lib/types/mobile/mobile-theme.d.ts.map +1 -0
  49. package/lib/types/mobile/mobile-theme.test.d.ts +2 -0
  50. package/lib/types/mobile/mobile-theme.test.d.ts.map +1 -0
  51. package/lib/types/mobile/mux.d.ts +52 -0
  52. package/lib/types/mobile/mux.d.ts.map +1 -0
  53. package/lib/types/mobile/rpc.d.ts +38 -0
  54. package/lib/types/mobile/rpc.d.ts.map +1 -0
  55. package/lib/types/mobile/theme-toggle.d.ts +12 -0
  56. package/lib/types/mobile/theme-toggle.d.ts.map +1 -0
  57. package/lib/types/mobile/views/App.d.ts +59 -0
  58. package/lib/types/mobile/views/App.d.ts.map +1 -0
  59. package/lib/types/mobile/views/ChatView.d.ts +39 -0
  60. package/lib/types/mobile/views/ChatView.d.ts.map +1 -0
  61. package/lib/types/mobile/views/ChatView.test.d.ts +2 -0
  62. package/lib/types/mobile/views/ChatView.test.d.ts.map +1 -0
  63. package/lib/types/mobile/views/SessionListView.d.ts +27 -0
  64. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -0
  65. package/lib/types/mobile/views/SessionListView.test.d.ts +2 -0
  66. package/lib/types/mobile/views/SessionListView.test.d.ts.map +1 -0
  67. package/lib/types/mobile/views/WorkspaceView.d.ts +19 -0
  68. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -0
  69. package/lib/types/mobile-api.d.ts +41 -0
  70. package/lib/types/mobile-api.d.ts.map +1 -0
  71. package/lib/types/mobile-routes.d.ts +16 -0
  72. package/lib/types/mobile-routes.d.ts.map +1 -0
  73. package/lib/types/pairing.d.ts +215 -0
  74. package/lib/types/pairing.d.ts.map +1 -0
  75. package/lib/types/routes.d.ts +58 -0
  76. package/lib/types/routes.d.ts.map +1 -0
  77. package/lib/types/tunnel.d.ts +99 -0
  78. package/lib/types/tunnel.d.ts.map +1 -0
  79. package/package.json +104 -0
  80. package/src/client/PairFailedNotice.tsx +33 -0
  81. package/src/client/PhoneIcon.tsx +31 -0
  82. package/src/client/PluginSettingsCard.tsx +207 -0
  83. package/src/client/RemoteEntry.tsx +202 -0
  84. package/src/client/RemotePanel.tsx +186 -0
  85. package/src/client/RemoteSettingsCard.tsx +220 -0
  86. package/src/client/deep-link.ts +137 -0
  87. package/src/client/index.ts +167 -0
  88. package/src/client/locales.ts +149 -0
  89. package/src/client/pair-api.ts +176 -0
  90. package/src/client/remote.module.css +422 -0
  91. package/src/client/settings-card.module.css +296 -0
  92. package/src/client/settings-form.ts +294 -0
  93. package/src/css-modules.d.ts +6 -0
  94. package/src/gate.ts +121 -0
  95. package/src/index.ts +299 -0
  96. package/src/invariant.ts +32 -0
  97. package/src/lan.ts +21 -0
  98. package/src/mobile/api.ts +106 -0
  99. package/src/mobile/index.tsx +27 -0
  100. package/src/mobile/messages.test.ts +222 -0
  101. package/src/mobile/messages.ts +555 -0
  102. package/src/mobile/mobile-styles.ts +853 -0
  103. package/src/mobile/mobile-theme.test.ts +84 -0
  104. package/src/mobile/mobile-theme.ts +76 -0
  105. package/src/mobile/mux.ts +126 -0
  106. package/src/mobile/rpc.ts +90 -0
  107. package/src/mobile/theme-toggle.tsx +47 -0
  108. package/src/mobile/views/App.tsx +152 -0
  109. package/src/mobile/views/ChatView.test.tsx +243 -0
  110. package/src/mobile/views/ChatView.tsx +650 -0
  111. package/src/mobile/views/SessionListView.test.tsx +139 -0
  112. package/src/mobile/views/SessionListView.tsx +186 -0
  113. package/src/mobile/views/WorkspaceView.tsx +108 -0
  114. package/src/mobile-api.ts +271 -0
  115. package/src/mobile-routes.ts +77 -0
  116. package/src/pairing.ts +390 -0
  117. package/src/routes.ts +346 -0
  118. package/src/tunnel.ts +264 -0
@@ -0,0 +1,243 @@
1
+ // @vitest-environment jsdom
2
+ /** ChatView: collapsible message folds, toolbar chips, and the bottom sheets. */
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
5
+ import type { SessionModels } from '@deepseek-ai/dsh-host-apiproxy/api/sessions'
6
+ import { ChatView } from './ChatView.tsx'
7
+ import { type SessionView } from './App.tsx'
8
+ import type { HistoryPage } from '../api.ts'
9
+ import type { WireEvent } from '../messages.ts'
10
+
11
+ // The api module is fully mocked; App.tsx's history wrapper is overridden to
12
+ // feed fixed history pages, its pure helpers (errorText / formatTime) stay real.
13
+ vi.mock('../api.ts', () => ({
14
+ models: vi.fn(),
15
+ selectModel: vi.fn(),
16
+ sendCommand: vi.fn(),
17
+ }))
18
+ vi.mock('./App.tsx', async importOriginal => {
19
+ const actual = await importOriginal<typeof import('./App.tsx')>()
20
+ return {
21
+ ...actual,
22
+ loadHistory: vi.fn(),
23
+ prompt: vi.fn(async () => {}),
24
+ }
25
+ })
26
+ import { models, selectModel, sendCommand } from '../api.ts'
27
+ import { loadHistory } from './App.tsx'
28
+
29
+ const session: SessionView = {
30
+ sessionId: 's-1',
31
+ title: '测试会话',
32
+ updatedAt: 1_700_000_000_000,
33
+ running: false,
34
+ blank: false,
35
+ }
36
+
37
+ /** Assemble one history entry wrapping a WireEvent (host history-page shape). */
38
+ function makeEntry(type: string, data: unknown, seq: number): { event: WireEvent } {
39
+ return { event: { type, seq, time: seq * 1_000, data } }
40
+ }
41
+
42
+ /** Build a history page from loose wire events (the host union is strict). */
43
+ function historyPage(events: Array<{ event: WireEvent }>, extra: Record<string, unknown> = {}): HistoryPage {
44
+ return { events: events as never, hasMore: false, ...extra } as HistoryPage
45
+ }
46
+
47
+ /** A full turn: user message, reasoning + text chunks, tool calls, final message. */
48
+ function turnEvents(): Array<{ event: WireEvent }> {
49
+ return [
50
+ makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: '改一下代码' }] }, 0),
51
+ makeEntry('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '先看结构' } }, 1),
52
+ makeEntry('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '\n再看细节' } }, 2),
53
+ makeEntry('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 1, text: '正在处理' } }, 3),
54
+ makeEntry('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{"cmd":"ls"}' }, 4),
55
+ makeEntry('assistant/message', {
56
+ turn: 0,
57
+ step: 0,
58
+ message: {
59
+ id: 'a-1',
60
+ role: 'assistant',
61
+ content: [
62
+ { type: 'reasoning', text: '先看结构\n再看细节' },
63
+ { type: 'text', text: '已完成修改' },
64
+ ],
65
+ },
66
+ }, 5),
67
+ ]
68
+ }
69
+
70
+ const modelsMock = vi.mocked(models)
71
+ const selectModelMock = vi.mocked(selectModel)
72
+ const sendCommandMock = vi.mocked(sendCommand)
73
+ const loadHistoryMock = vi.mocked(loadHistory)
74
+
75
+ beforeEach(() => {
76
+ modelsMock.mockResolvedValue({
77
+ current: { provider: 'fx', model: 'fx-1' },
78
+ routable: true,
79
+ groups: [
80
+ {
81
+ id: 'fx',
82
+ name: 'FX',
83
+ models: [
84
+ { id: 'fx-1', name: 'FX 标准' },
85
+ { id: 'fx-2', name: 'FX 深度', reasoning: { efforts: [{ id: 'high', name: '高' }], defaultEffort: 'high' } },
86
+ ],
87
+ },
88
+ ],
89
+ failures: [],
90
+ } satisfies SessionModels)
91
+ selectModelMock.mockResolvedValue({ selected: { provider: 'fx', model: 'fx-2', reasoningEffort: 'high' } })
92
+ sendCommandMock.mockResolvedValue({})
93
+ })
94
+
95
+ afterEach(() => {
96
+ cleanup()
97
+ vi.clearAllMocks()
98
+ })
99
+
100
+ describe('ChatView message folds', () => {
101
+ it('hides reasoning behind a collapsed disclosure and expands on tap', async () => {
102
+ loadHistoryMock.mockResolvedValue(historyPage(turnEvents()))
103
+ render(<ChatView session={session} onBack={() => {}} />)
104
+
105
+ // The folded turn renders: user bubble, assistant text, disclosures.
106
+ expect(await screen.findByText('改一下代码')).toBeTruthy()
107
+ expect(await screen.findByText('已完成修改')).toBeTruthy()
108
+ const head = await screen.findByRole('button', { name: /深度思考/ })
109
+ expect(head.getAttribute('aria-expanded')).toBe('false')
110
+ // Only the one-line summary shows while collapsed; the body stays hidden.
111
+ expect(await screen.findByText('先看结构')).toBeTruthy()
112
+ expect(screen.queryByText(/再看细节/)).toBeNull()
113
+
114
+ fireEvent.click(head)
115
+ expect(head.getAttribute('aria-expanded')).toBe('true')
116
+ expect(await screen.findByText(/再看细节/)).toBeTruthy()
117
+ })
118
+
119
+ it('keeps the tool disclosure collapsed with a summary, then reveals arguments', async () => {
120
+ loadHistoryMock.mockResolvedValue(historyPage(turnEvents()))
121
+ render(<ChatView session={session} onBack={() => {}} />)
122
+
123
+ const head = await screen.findByRole('button', { name: /工具/ })
124
+ expect(head.getAttribute('aria-expanded')).toBe('false')
125
+ expect(screen.queryByText('{"cmd":"ls"}')).toBeNull()
126
+
127
+ fireEvent.click(head)
128
+ expect(head.getAttribute('aria-expanded')).toBe('true')
129
+ expect(await screen.findByText('{"cmd":"ls"}')).toBeTruthy()
130
+ expect(screen.getByText('bash')).toBeTruthy()
131
+ })
132
+
133
+ it('shows the permission chip from the history-tail projection and applies via /permission', async () => {
134
+ loadHistoryMock.mockResolvedValue(historyPage(turnEvents(), {
135
+ projections: {
136
+ asOfSeq: 4,
137
+ values: {
138
+ permissions: {
139
+ options: [
140
+ { value: 'read-only', name: '只读' },
141
+ { value: 'workspace-write', name: '读写工作区' },
142
+ ],
143
+ currentValue: 'read-only',
144
+ },
145
+ } as Record<string, unknown>,
146
+ },
147
+ }))
148
+ render(<ChatView session={session} onBack={() => {}} />)
149
+
150
+ const chip = await screen.findByRole('button', { name: /只读/ })
151
+ fireEvent.click(chip)
152
+ // The sheet lists the presets; picking one dispatches the slash command.
153
+ const writeOption = await screen.findByRole('button', { name: /读写工作区/ })
154
+ fireEvent.click(writeOption)
155
+ await waitFor(() => {
156
+ expect(sendCommandMock).toHaveBeenCalledWith('s-1', '/permission workspace-write')
157
+ })
158
+ })
159
+
160
+ it('requires an explicit confirm before enabling full access', async () => {
161
+ loadHistoryMock.mockResolvedValue(historyPage(turnEvents(), {
162
+ projections: {
163
+ asOfSeq: 4,
164
+ values: {
165
+ permissions: {
166
+ options: [{ value: 'danger-full-access', name: '完全权限' }],
167
+ currentValue: 'workspace-write',
168
+ },
169
+ } as Record<string, unknown>,
170
+ },
171
+ }))
172
+ render(<ChatView session={session} onBack={() => {}} />)
173
+
174
+ // The chip shows the derived label for the unmatched current value.
175
+ fireEvent.click(await screen.findByRole('button', { name: /Workspace Write/ }))
176
+ // Picking full access opens the confirmation sheet instead of submitting.
177
+ fireEvent.click(await screen.findByRole('button', { name: /完全权限/ }))
178
+ expect(await screen.findByText(/确认完全权限/)).toBeTruthy()
179
+ expect(sendCommandMock).not.toHaveBeenCalled()
180
+ // Cancelling dispatches nothing; opening again and confirming submits.
181
+ fireEvent.click(screen.getByRole('button', { name: /取消/ }))
182
+ fireEvent.click(screen.getByRole('button', { name: /完全权限/ }))
183
+ fireEvent.click(await screen.findByRole('button', { name: /确认开启/ }))
184
+ await waitFor(() => {
185
+ expect(sendCommandMock).toHaveBeenCalledWith('s-1', '/permission danger-full-access')
186
+ })
187
+ })
188
+ })
189
+
190
+ describe('ChatView model sheet', () => {
191
+ it('labels the toolbar chip with the current model and selects a new one', async () => {
192
+ loadHistoryMock.mockResolvedValue(historyPage(turnEvents()))
193
+ render(<ChatView session={session} onBack={() => {}} />)
194
+
195
+ const chip = await screen.findByRole('button', { name: /模型/ })
196
+ expect(chip.textContent).toContain('fx-1')
197
+
198
+ fireEvent.click(chip)
199
+ const deep = await screen.findByRole('button', { name: /FX 深度/ })
200
+ fireEvent.click(deep)
201
+ await waitFor(() => {
202
+ expect(selectModelMock).toHaveBeenCalledWith('s-1', { provider: 'fx', model: 'fx-2', reasoningEffort: 'high' })
203
+ })
204
+ })
205
+
206
+ it('offers effort choices for the current model and submits the picked effort', async () => {
207
+ loadHistoryMock.mockResolvedValue(historyPage(turnEvents()))
208
+ // The current model already is the effort-capable one.
209
+ modelsMock.mockResolvedValue({
210
+ current: { provider: 'fx', model: 'fx-2', reasoningEffort: 'high' },
211
+ routable: true,
212
+ groups: [
213
+ {
214
+ id: 'fx',
215
+ name: 'FX',
216
+ models: [
217
+ { id: 'fx-1', name: 'FX 标准' },
218
+ { id: 'fx-2', name: 'FX 深度', reasoning: { efforts: [{ id: 'high', name: '高' }], defaultEffort: 'high' } },
219
+ ],
220
+ },
221
+ ],
222
+ failures: [],
223
+ } satisfies SessionModels)
224
+ render(<ChatView session={session} onBack={() => {}} />)
225
+
226
+ fireEvent.click(await screen.findByRole('button', { name: /模型/ }))
227
+ const effort = await screen.findByRole('button', { name: /^高/ })
228
+ fireEvent.click(effort)
229
+ await waitFor(() => {
230
+ expect(selectModelMock).toHaveBeenCalledWith('s-1', { provider: 'fx', model: 'fx-2', reasoningEffort: 'high' })
231
+ })
232
+ })
233
+
234
+ it('explains a transport 403 on the model channel as a stale host', async () => {
235
+ loadHistoryMock.mockResolvedValue(historyPage(turnEvents()))
236
+ modelsMock.mockRejectedValue(new Error('HTTP 403'))
237
+ render(<ChatView session={session} onBack={() => {}} />)
238
+
239
+ fireEvent.click(await screen.findByRole('button', { name: /模型/ }))
240
+ expect(await screen.findByText(/HTTP 403/)).toBeTruthy()
241
+ expect(await screen.findByText(/重启 dsh web/)).toBeTruthy()
242
+ })
243
+ })