@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,222 @@
1
+ /** foldEvents: message-list folding from a session event stream. */
2
+ import { describe, expect, it } from 'vitest'
3
+ import { foldEvents, type WireEvent } from './messages.ts'
4
+
5
+ /** Assemble one event with an auto-incrementing seq / time. */
6
+ function makeEvent(
7
+ type: string,
8
+ data: unknown,
9
+ seq: number,
10
+ time = seq * 1_000,
11
+ ): WireEvent {
12
+ return { type, seq, time, data }
13
+ }
14
+
15
+ /** A DSH-shaped user message payload (content: text blocks). */
16
+ function userMessageData(id: string, text: string): unknown {
17
+ return { id, role: 'user', content: [{ type: 'text', text }], source: { kind: 'user' } }
18
+ }
19
+
20
+ /** A DSH-shaped assistant message payload for one step. */
21
+ function assistantMessageData(
22
+ id: string,
23
+ turn: number,
24
+ step: number,
25
+ text: string,
26
+ ): unknown {
27
+ return {
28
+ turn,
29
+ step,
30
+ message: {
31
+ id,
32
+ role: 'assistant',
33
+ content: [{ type: 'text', text }],
34
+ source: { kind: 'model', provider: 'fx', model: 'fx-1' },
35
+ },
36
+ }
37
+ }
38
+
39
+ /** A DSH-shaped text-delta chunk for one step. */
40
+ function textChunk(turn: number, step: number, text: string, seq: number): WireEvent {
41
+ return makeEvent('assistant/chunk', { turn, step, chunk: { type: 'text-delta', index: 0, text } }, seq)
42
+ }
43
+
44
+ describe('foldEvents', () => {
45
+ it('folds one full turn: user message, streamed chunks, final assistant message', () => {
46
+ const events: WireEvent[] = [
47
+ makeEvent('user/message', userMessageData('u-1', '查一下天气'), 0),
48
+ makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '今天' } }, 1),
49
+ makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '晴天' } }, 2),
50
+ makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '今天晴天'), 3),
51
+ ]
52
+ const result = foldEvents(events)
53
+ expect(result).toHaveLength(2)
54
+ expect(result[0]).toMatchObject({ kind: 'user', id: 'u-1', text: '查一下天气', seq: 0 })
55
+ expect(result[1]).toMatchObject({
56
+ kind: 'assistant',
57
+ id: 'a-1',
58
+ text: '今天晴天',
59
+ seq: 3,
60
+ pending: false,
61
+ })
62
+ })
63
+
64
+ it('keeps the streamed pending assistant alive while chunks arrive, then finalizes', () => {
65
+ const first = foldEvents([
66
+ makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
67
+ textChunk(0, 0, '你', 1),
68
+ textChunk(0, 0, '好', 2),
69
+ ])
70
+ const pending = first.find(message => message.kind === 'assistant')
71
+ expect(pending).toMatchObject({ text: '你好', pending: true, seq: 2 })
72
+
73
+ const finalized = foldEvents(
74
+ [makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '你好!'), 3)],
75
+ first,
76
+ )
77
+ const assistant = finalized.find(message => message.kind === 'assistant')
78
+ expect(assistant).toMatchObject({ id: 'a-1', text: '你好!', pending: false, seq: 3 })
79
+ // No duplicate assistant message appears.
80
+ expect(finalized.filter(message => message.kind === 'assistant')).toHaveLength(1)
81
+ })
82
+
83
+ it('message/update replaces text and message/delete removes the row', () => {
84
+ const events: WireEvent[] = [
85
+ makeEvent('user/message', userMessageData('u-1', '旧文本'), 0),
86
+ makeEvent('user/message', userMessageData('u-2', '要删掉'), 1),
87
+ makeEvent('message/update', { id: 'u-1', text: '新文本' }, 2),
88
+ makeEvent('message/delete', { id: 'u-2' }, 3),
89
+ ]
90
+ const result = foldEvents(events)
91
+ expect(result.map(message => message.id)).toEqual(['u-1'])
92
+ expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user', text: '新文本', seq: 2 })
93
+ })
94
+
95
+ it('accumulates toolSummary on the assistant message owning the tool calls', () => {
96
+ const events: WireEvent[] = [
97
+ makeEvent('user/message', userMessageData('u-1', '改文件'), 0),
98
+ textChunk(0, 0, '正在处理', 1),
99
+ makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, 2),
100
+ makeEvent('tool/call', { turn: 0, step: 0, callId: 'c2', name: 'read', arguments: '{"path":"a.txt"}' }, 3),
101
+ makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '已完成'), 4),
102
+ ]
103
+ const result = foldEvents(events)
104
+ const assistant = result.find(message => message.kind === 'assistant')
105
+ expect(assistant?.toolSummary).toBe('使用 bash / read')
106
+ expect(assistant?.tools).toEqual([
107
+ { callId: 'c1', name: 'bash', arguments: '{}' },
108
+ { callId: 'c2', name: 'read', arguments: '{"path":"a.txt"}' },
109
+ ])
110
+ })
111
+
112
+ it('keeps reasoning text apart from the message body and folds reasoning-delta chunks', () => {
113
+ const events: WireEvent[] = [
114
+ makeEvent('user/message', userMessageData('u-1', '复杂问题'), 0),
115
+ makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '先分析' } }, 1),
116
+ makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '再行动' } }, 2),
117
+ makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 1, text: '结论' } }, 3),
118
+ makeEvent('assistant/message', {
119
+ turn: 0,
120
+ step: 0,
121
+ message: {
122
+ id: 'a-1',
123
+ role: 'assistant',
124
+ content: [
125
+ { type: 'reasoning', text: '先分析再行动' },
126
+ { type: 'text', text: '结论' },
127
+ ],
128
+ source: { kind: 'model', provider: 'fx', model: 'fx-1' },
129
+ },
130
+ }, 4),
131
+ ]
132
+ const result = foldEvents(events)
133
+ const assistant = result.find(message => message.kind === 'assistant')
134
+ expect(assistant).toMatchObject({ text: '结论', reasoning: '先分析再行动', pending: false })
135
+ })
136
+
137
+ it('keeps streamed reasoning when the final assistant message omits the reasoning block', () => {
138
+ const streamed = foldEvents([
139
+ makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
140
+ makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '思考过程' } }, 1),
141
+ ])
142
+ const finalized = foldEvents(
143
+ [makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '回答'), 2)],
144
+ streamed,
145
+ )
146
+ const assistant = finalized.find(message => message.kind === 'assistant')
147
+ expect(assistant).toMatchObject({ text: '回答', reasoning: '思考过程', pending: false })
148
+ })
149
+
150
+ it('dedupes repeated tool/call events by callId', () => {
151
+ const events: WireEvent[] = [
152
+ makeEvent('user/message', userMessageData('u-1', '跑测试'), 0),
153
+ textChunk(0, 0, '开始', 1),
154
+ makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }, 2),
155
+ makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }, 3),
156
+ makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'done'), 4),
157
+ ]
158
+ const result = foldEvents(events)
159
+ const assistant = result.find(message => message.kind === 'assistant')
160
+ expect(assistant?.tools).toEqual([{ callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }])
161
+ })
162
+
163
+ it('marks the assistant message failed when turn/end ends in an error', () => {
164
+ const events: WireEvent[] = [
165
+ makeEvent('user/message', userMessageData('u-1', 'hello'), 0),
166
+ makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '部分' } }, 1),
167
+ makeEvent('turn/end', { turn: 0, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } }, 2),
168
+ ]
169
+ const result = foldEvents(events)
170
+ const assistant = result.find(message => message.kind === 'assistant')
171
+ expect(assistant).toMatchObject({ failed: true, pending: false, kind: 'assistant' })
172
+ })
173
+
174
+ it('does not flag failed for a completed turn', () => {
175
+ const events: WireEvent[] = [
176
+ makeEvent('user/message', userMessageData('u-1', 'hello'), 0),
177
+ makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'ok'), 1),
178
+ makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 2),
179
+ ]
180
+ const result = foldEvents(events)
181
+ const assistant = result.find(message => message.kind === 'assistant')
182
+ expect(assistant?.failed).toBeFalsy()
183
+ expect(assistant?.pending).toBeFalsy()
184
+ })
185
+
186
+ it('is idempotent: applying the same batch twice yields an identical list', () => {
187
+ const events: WireEvent[] = [
188
+ makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
189
+ textChunk(0, 0, '一', 1),
190
+ textChunk(0, 0, '二', 2),
191
+ makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, 3),
192
+ makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '一二'), 4),
193
+ makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 5),
194
+ ]
195
+ const once = foldEvents(events)
196
+ const twice = foldEvents(events, once)
197
+ expect(twice).toEqual(once)
198
+ expect(twice.filter(message => message.kind === 'assistant')).toHaveLength(1)
199
+ // Text must not have doubled from re-aggregating the streamed chunks.
200
+ expect(twice.find(message => message.kind === 'assistant')?.text).toBe('一二')
201
+ })
202
+
203
+ it('skips unknown / unsupported event types safely', () => {
204
+ const events: WireEvent[] = [
205
+ makeEvent('session/end-seed', {}, 0),
206
+ makeEvent('turn/start', { turn: 0 }, 1),
207
+ makeEvent('user/message', userMessageData('u-1', 'hello'), 2),
208
+ makeEvent('some/future-plugin', { whatever: true }, 3),
209
+ makeEvent('goal/change', { objective: 'x' }, 4),
210
+ ]
211
+ const result = foldEvents(events)
212
+ expect(result).toHaveLength(1)
213
+ expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user' })
214
+ })
215
+
216
+ it('does not mutate the caller-supplied existing list', () => {
217
+ const first = foldEvents([makeEvent('user/message', userMessageData('u-1', 'hello'), 0)])
218
+ const snapshot = JSON.stringify(first)
219
+ foldEvents([makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'world'), 1)], first)
220
+ expect(JSON.stringify(first)).toBe(snapshot)
221
+ })
222
+ })