@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.
Files changed (238) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +77 -468
  3. package/README.zh.md +83 -113
  4. package/lib/client.js +993 -203
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1136 -1466
  7. package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
  8. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
  9. package/lib/types/client/FooterRemoteEntry.js +1 -1
  10. package/lib/types/client/RemoteEntry.d.ts +7 -4
  11. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  12. package/lib/types/client/RemoteEntry.js +7 -10
  13. package/lib/types/client/RemotePanel.d.ts +2 -2
  14. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  15. package/lib/types/client/RemotePanel.js +2 -2
  16. package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
  17. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  18. package/lib/types/client/RemoteSettingsCard.js +56 -3
  19. package/lib/types/client/deep-link.d.ts +10 -13
  20. package/lib/types/client/deep-link.d.ts.map +1 -1
  21. package/lib/types/client/deep-link.js +14 -74
  22. package/lib/types/client/index.d.ts +2 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +79 -39
  25. package/lib/types/client/locales.d.ts +32 -12
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +42 -22
  28. package/lib/types/client/mobile-adapt.d.ts +49 -0
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -0
  30. package/lib/types/client/mobile-adapt.js +1002 -0
  31. package/lib/types/client/pair-api.d.ts +24 -6
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +9 -14
  34. package/lib/types/client/remote-channel.d.ts +3 -0
  35. package/lib/types/client/remote-channel.d.ts.map +1 -1
  36. package/lib/types/client/remote-channel.js +35 -3
  37. package/lib/types/client/settings-form.d.ts +35 -43
  38. package/lib/types/client/settings-form.d.ts.map +1 -1
  39. package/lib/types/client/settings-form.js +60 -53
  40. package/lib/types/firewall.d.ts +75 -0
  41. package/lib/types/firewall.d.ts.map +1 -0
  42. package/lib/types/firewall.js +184 -0
  43. package/lib/types/gate.d.ts +11 -5
  44. package/lib/types/gate.d.ts.map +1 -1
  45. package/lib/types/gate.js +11 -5
  46. package/lib/types/index.d.ts +39 -10
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/index.js +206 -58
  49. package/lib/types/inner-auth.d.ts +32 -0
  50. package/lib/types/inner-auth.d.ts.map +1 -0
  51. package/lib/types/inner-auth.js +72 -0
  52. package/lib/types/lan-bind-plan.d.ts +55 -0
  53. package/lib/types/lan-bind-plan.d.ts.map +1 -0
  54. package/lib/types/lan-bind-plan.js +60 -0
  55. package/lib/types/lan-bind.d.ts +69 -0
  56. package/lib/types/lan-bind.d.ts.map +1 -0
  57. package/lib/types/lan-bind.js +128 -0
  58. package/lib/types/loopback-proxy.d.ts +9 -2
  59. package/lib/types/loopback-proxy.d.ts.map +1 -1
  60. package/lib/types/loopback-proxy.js +24 -2
  61. package/lib/types/pairing.d.ts +15 -3
  62. package/lib/types/pairing.d.ts.map +1 -1
  63. package/lib/types/pairing.js +20 -6
  64. package/lib/types/remote-api.d.ts +30 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +55 -42
  67. package/lib/types/remote-channel-boot.d.ts +21 -2
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +65 -3
  70. package/lib/types/remote-channel-rules.d.ts +8 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +9 -2
  73. package/lib/types/remote-methods.d.ts +45 -11
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +56 -27
  76. package/lib/types/remote-presence-pet.d.ts +51 -0
  77. package/lib/types/remote-presence-pet.d.ts.map +1 -0
  78. package/lib/types/remote-presence-pet.js +101 -0
  79. package/lib/types/routes.d.ts +76 -4
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +336 -19
  82. package/lib/types/tunnel.d.ts.map +1 -1
  83. package/lib/types/tunnel.js +7 -1
  84. package/package.json +20 -18
  85. package/src/client/FooterRemoteEntry.tsx +9 -10
  86. package/src/client/RemoteEntry.tsx +13 -14
  87. package/src/client/RemotePanel.tsx +5 -16
  88. package/src/client/RemoteSettingsCard.tsx +72 -13
  89. package/src/client/deep-link.ts +14 -75
  90. package/src/client/index.ts +82 -39
  91. package/src/client/locales.ts +42 -22
  92. package/src/client/mobile-adapt.ts +954 -0
  93. package/src/client/pair-api.ts +26 -15
  94. package/src/client/remote-channel.ts +31 -3
  95. package/src/client/settings-form.ts +78 -92
  96. package/src/dsh-home.ts +1 -1
  97. package/src/firewall.ts +230 -0
  98. package/src/gate.ts +11 -5
  99. package/src/index.ts +235 -65
  100. package/src/inner-auth.ts +85 -0
  101. package/src/lan-bind-plan.ts +81 -0
  102. package/src/lan-bind.ts +145 -0
  103. package/src/loopback-proxy.ts +34 -0
  104. package/src/pairing.persist.test.ts +1 -1
  105. package/src/pairing.ts +20 -6
  106. package/src/remote-api.ts +67 -45
  107. package/src/remote-channel-boot.ts +70 -3
  108. package/src/remote-channel-rules.ts +17 -2
  109. package/src/remote-methods.ts +58 -27
  110. package/src/remote-presence-pet.ts +129 -0
  111. package/src/routes.ts +358 -19
  112. package/src/tunnel.ts +7 -1
  113. package/assets/apple-touch-icon.png +0 -0
  114. package/assets/mobile-icon-192.png +0 -0
  115. package/assets/mobile-icon-512.png +0 -0
  116. package/assets/mobile-service-worker.js +0 -63
  117. package/lib/mobile.js +0 -17658
  118. package/lib/mobile.js.map +0 -1
  119. package/lib/types/mobile/PairRequiredView.d.ts +0 -7
  120. package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
  121. package/lib/types/mobile/PairRequiredView.js +0 -28
  122. package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
  123. package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
  124. package/lib/types/mobile/PairRequiredView.test.js +0 -24
  125. package/lib/types/mobile/api.d.ts +0 -145
  126. package/lib/types/mobile/api.d.ts.map +0 -1
  127. package/lib/types/mobile/api.js +0 -103
  128. package/lib/types/mobile/index.d.ts +0 -10
  129. package/lib/types/mobile/index.d.ts.map +0 -1
  130. package/lib/types/mobile/index.js +0 -38
  131. package/lib/types/mobile/markdown.d.ts +0 -34
  132. package/lib/types/mobile/markdown.d.ts.map +0 -1
  133. package/lib/types/mobile/markdown.js +0 -285
  134. package/lib/types/mobile/markdown.test.d.ts +0 -2
  135. package/lib/types/mobile/markdown.test.d.ts.map +0 -1
  136. package/lib/types/mobile/markdown.test.js +0 -79
  137. package/lib/types/mobile/messages.d.ts +0 -149
  138. package/lib/types/mobile/messages.d.ts.map +0 -1
  139. package/lib/types/mobile/messages.js +0 -561
  140. package/lib/types/mobile/messages.test.d.ts +0 -2
  141. package/lib/types/mobile/messages.test.d.ts.map +0 -1
  142. package/lib/types/mobile/messages.test.js +0 -356
  143. package/lib/types/mobile/mobile-styles.d.ts +0 -6
  144. package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
  145. package/lib/types/mobile/mobile-styles.js +0 -1438
  146. package/lib/types/mobile/mobile-theme.d.ts +0 -22
  147. package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
  148. package/lib/types/mobile/mobile-theme.js +0 -70
  149. package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
  150. package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
  151. package/lib/types/mobile/mobile-theme.test.js +0 -71
  152. package/lib/types/mobile/mux.d.ts +0 -128
  153. package/lib/types/mobile/mux.d.ts.map +0 -1
  154. package/lib/types/mobile/mux.js +0 -305
  155. package/lib/types/mobile/mux.test.d.ts +0 -2
  156. package/lib/types/mobile/mux.test.d.ts.map +0 -1
  157. package/lib/types/mobile/mux.test.js +0 -267
  158. package/lib/types/mobile/pairing.d.ts +0 -31
  159. package/lib/types/mobile/pairing.d.ts.map +0 -1
  160. package/lib/types/mobile/pairing.js +0 -62
  161. package/lib/types/mobile/rpc.d.ts +0 -38
  162. package/lib/types/mobile/rpc.d.ts.map +0 -1
  163. package/lib/types/mobile/rpc.js +0 -83
  164. package/lib/types/mobile/theme-toggle.d.ts +0 -12
  165. package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
  166. package/lib/types/mobile/theme-toggle.js +0 -26
  167. package/lib/types/mobile/views/App.d.ts +0 -57
  168. package/lib/types/mobile/views/App.d.ts.map +0 -1
  169. package/lib/types/mobile/views/App.js +0 -150
  170. package/lib/types/mobile/views/App.test.d.ts +0 -2
  171. package/lib/types/mobile/views/App.test.d.ts.map +0 -1
  172. package/lib/types/mobile/views/App.test.js +0 -52
  173. package/lib/types/mobile/views/ChatView.d.ts +0 -63
  174. package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
  175. package/lib/types/mobile/views/ChatView.js +0 -821
  176. package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
  177. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
  178. package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
  179. package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
  180. package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
  181. package/lib/types/mobile/views/ChatView.test.js +0 -718
  182. package/lib/types/mobile/views/SessionListView.d.ts +0 -27
  183. package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
  184. package/lib/types/mobile/views/SessionListView.js +0 -139
  185. package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
  186. package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
  187. package/lib/types/mobile/views/SessionListView.test.js +0 -167
  188. package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
  189. package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
  190. package/lib/types/mobile/views/WorkspaceView.js +0 -93
  191. package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
  192. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
  193. package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
  194. package/lib/types/mobile-api.d.ts +0 -65
  195. package/lib/types/mobile-api.d.ts.map +0 -1
  196. package/lib/types/mobile-api.js +0 -404
  197. package/lib/types/mobile-pending.d.ts +0 -38
  198. package/lib/types/mobile-pending.d.ts.map +0 -1
  199. package/lib/types/mobile-pending.js +0 -59
  200. package/lib/types/mobile-pwa.d.ts +0 -6
  201. package/lib/types/mobile-pwa.d.ts.map +0 -1
  202. package/lib/types/mobile-pwa.js +0 -17
  203. package/lib/types/mobile-routes.d.ts +0 -13
  204. package/lib/types/mobile-routes.d.ts.map +0 -1
  205. package/lib/types/mobile-routes.js +0 -217
  206. package/lib/types/paired-model-catalog.d.ts +0 -17
  207. package/lib/types/paired-model-catalog.d.ts.map +0 -1
  208. package/lib/types/paired-model-catalog.js +0 -311
  209. package/src/mobile/PairRequiredView.test.tsx +0 -28
  210. package/src/mobile/PairRequiredView.tsx +0 -58
  211. package/src/mobile/api.ts +0 -229
  212. package/src/mobile/index.tsx +0 -41
  213. package/src/mobile/markdown.test.ts +0 -91
  214. package/src/mobile/markdown.ts +0 -291
  215. package/src/mobile/messages.test.ts +0 -402
  216. package/src/mobile/messages.ts +0 -685
  217. package/src/mobile/mobile-styles.ts +0 -1439
  218. package/src/mobile/mobile-theme.test.ts +0 -84
  219. package/src/mobile/mobile-theme.ts +0 -76
  220. package/src/mobile/mux.test.ts +0 -314
  221. package/src/mobile/mux.ts +0 -336
  222. package/src/mobile/pairing.ts +0 -77
  223. package/src/mobile/rpc.ts +0 -90
  224. package/src/mobile/theme-toggle.tsx +0 -47
  225. package/src/mobile/views/App.test.tsx +0 -62
  226. package/src/mobile/views/App.tsx +0 -213
  227. package/src/mobile/views/ChatView.stream.test.tsx +0 -268
  228. package/src/mobile/views/ChatView.test.tsx +0 -816
  229. package/src/mobile/views/ChatView.tsx +0 -1342
  230. package/src/mobile/views/SessionListView.test.tsx +0 -207
  231. package/src/mobile/views/SessionListView.tsx +0 -244
  232. package/src/mobile/views/WorkspaceView.test.tsx +0 -227
  233. package/src/mobile/views/WorkspaceView.tsx +0 -245
  234. package/src/mobile-api.ts +0 -418
  235. package/src/mobile-pending.ts +0 -88
  236. package/src/mobile-pwa.ts +0 -19
  237. package/src/mobile-routes.ts +0 -231
  238. package/src/paired-model-catalog.ts +0 -272
@@ -1,356 +0,0 @@
1
- /** foldEvents: message-list folding from a session event stream. */
2
- import { describe, expect, it } from 'vitest';
3
- import { EventFolder, foldEvents } from "./messages.js";
4
- /** Assemble one event with an auto-incrementing seq / time. */
5
- function makeEvent(type, data, seq, time = seq * 1_000) {
6
- return { type, seq, time, data };
7
- }
8
- /** A DSH-shaped user message payload (content: text blocks). */
9
- function userMessageData(id, text) {
10
- return { id, role: 'user', content: [{ type: 'text', text }], source: { kind: 'user' } };
11
- }
12
- /** A DSH-shaped assistant message payload for one step. */
13
- function assistantMessageData(id, turn, step, text) {
14
- return {
15
- turn,
16
- step,
17
- message: {
18
- id,
19
- role: 'assistant',
20
- content: [{ type: 'text', text }],
21
- source: { kind: 'model', provider: 'fx', model: 'fx-1' },
22
- },
23
- };
24
- }
25
- /** A DSH-shaped text-delta chunk for one step. */
26
- function textChunk(turn, step, text, seq) {
27
- return makeEvent('assistant/chunk', { turn, step, chunk: { type: 'text-delta', index: 0, text } }, seq);
28
- }
29
- describe('foldEvents', () => {
30
- it('folds one full turn: user message, streamed chunks, final assistant message', () => {
31
- const events = [
32
- makeEvent('user/message', userMessageData('u-1', '查一下天气'), 0),
33
- makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '今天' } }, 1),
34
- makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '晴天' } }, 2),
35
- makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '今天晴天'), 3),
36
- ];
37
- const result = foldEvents(events);
38
- expect(result).toHaveLength(2);
39
- expect(result[0]).toMatchObject({ kind: 'user', id: 'u-1', text: '查一下天气', seq: 0 });
40
- expect(result[1]).toMatchObject({
41
- kind: 'assistant',
42
- id: 'a-1',
43
- text: '今天晴天',
44
- seq: 3,
45
- pending: false,
46
- });
47
- });
48
- it('keeps the streamed pending assistant alive while chunks arrive, then finalizes', () => {
49
- const first = foldEvents([
50
- makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
51
- textChunk(0, 0, '你', 1),
52
- textChunk(0, 0, '好', 2),
53
- ]);
54
- const pending = first.find(message => message.kind === 'assistant');
55
- expect(pending).toMatchObject({ text: '你好', pending: true, seq: 2 });
56
- const finalized = foldEvents([makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '你好!'), 3)], first);
57
- const assistant = finalized.find(message => message.kind === 'assistant');
58
- expect(assistant).toMatchObject({ id: 'a-1', text: '你好!', pending: false, seq: 3 });
59
- // No duplicate assistant message appears.
60
- expect(finalized.filter(message => message.kind === 'assistant')).toHaveLength(1);
61
- });
62
- it('message/update replaces text and message/delete removes the row', () => {
63
- const events = [
64
- makeEvent('user/message', userMessageData('u-1', '旧文本'), 0),
65
- makeEvent('user/message', userMessageData('u-2', '要删掉'), 1),
66
- makeEvent('message/update', { id: 'u-1', text: '新文本' }, 2),
67
- makeEvent('message/delete', { id: 'u-2' }, 3),
68
- ];
69
- const result = foldEvents(events);
70
- expect(result.map(message => message.id)).toEqual(['u-1']);
71
- expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user', text: '新文本', seq: 2 });
72
- });
73
- it('accumulates toolSummary on the assistant message owning the tool calls', () => {
74
- const events = [
75
- makeEvent('user/message', userMessageData('u-1', '改文件'), 0),
76
- textChunk(0, 0, '正在处理', 1),
77
- makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, 2),
78
- makeEvent('tool/call', { turn: 0, step: 0, callId: 'c2', name: 'read', arguments: '{"path":"a.txt"}' }, 3),
79
- makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '已完成'), 4),
80
- ];
81
- const result = foldEvents(events);
82
- const assistant = result.find(message => message.kind === 'assistant');
83
- expect(assistant?.toolSummary).toBe('使用 bash / read');
84
- expect(assistant?.tools).toEqual([
85
- { callId: 'c1', name: 'bash', arguments: '{}' },
86
- { callId: 'c2', name: 'read', arguments: '{"path":"a.txt"}' },
87
- ]);
88
- });
89
- it('keeps reasoning text apart from the message body and folds reasoning-delta chunks', () => {
90
- const events = [
91
- makeEvent('user/message', userMessageData('u-1', '复杂问题'), 0),
92
- makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '先分析' } }, 1),
93
- makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '再行动' } }, 2),
94
- makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 1, text: '结论' } }, 3),
95
- makeEvent('assistant/message', {
96
- turn: 0,
97
- step: 0,
98
- message: {
99
- id: 'a-1',
100
- role: 'assistant',
101
- content: [
102
- { type: 'reasoning', text: '先分析再行动' },
103
- { type: 'text', text: '结论' },
104
- ],
105
- source: { kind: 'model', provider: 'fx', model: 'fx-1' },
106
- },
107
- }, 4),
108
- ];
109
- const result = foldEvents(events);
110
- const assistant = result.find(message => message.kind === 'assistant');
111
- expect(assistant).toMatchObject({ text: '结论', reasoning: '先分析再行动', pending: false });
112
- });
113
- it('keeps streamed reasoning when the final assistant message omits the reasoning block', () => {
114
- const streamed = foldEvents([
115
- makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
116
- makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'reasoning-delta', index: 0, text: '思考过程' } }, 1),
117
- ]);
118
- const finalized = foldEvents([makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '回答'), 2)], streamed);
119
- const assistant = finalized.find(message => message.kind === 'assistant');
120
- expect(assistant).toMatchObject({ text: '回答', reasoning: '思考过程', pending: false });
121
- });
122
- it('dedupes repeated tool/call events by callId', () => {
123
- const events = [
124
- makeEvent('user/message', userMessageData('u-1', '跑测试'), 0),
125
- textChunk(0, 0, '开始', 1),
126
- makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }, 2),
127
- makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }, 3),
128
- makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'done'), 4),
129
- ];
130
- const result = foldEvents(events);
131
- const assistant = result.find(message => message.kind === 'assistant');
132
- expect(assistant?.tools).toEqual([{ callId: 'c1', name: 'bash', arguments: '{"cmd":"a"}' }]);
133
- });
134
- it('marks the assistant message failed when turn/end ends in an error', () => {
135
- const events = [
136
- makeEvent('user/message', userMessageData('u-1', 'hello'), 0),
137
- makeEvent('assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text: '部分' } }, 1),
138
- makeEvent('turn/end', { turn: 0, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } }, 2),
139
- ];
140
- const result = foldEvents(events);
141
- const assistant = result.find(message => message.kind === 'assistant');
142
- expect(assistant).toMatchObject({ failed: true, pending: false, kind: 'assistant' });
143
- });
144
- it('does not flag failed for a completed turn', () => {
145
- const events = [
146
- makeEvent('user/message', userMessageData('u-1', 'hello'), 0),
147
- makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'ok'), 1),
148
- makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 2),
149
- ];
150
- const result = foldEvents(events);
151
- const assistant = result.find(message => message.kind === 'assistant');
152
- expect(assistant?.failed).toBeFalsy();
153
- expect(assistant?.pending).toBeFalsy();
154
- });
155
- it('keeps same-turn steps in final-event order after turn/end', () => {
156
- const result = foldEvents([
157
- makeEvent('user/message', userMessageData('u-1', 'multi-step'), 0),
158
- makeEvent('assistant/message', assistantMessageData('step-z', 0, 0, 'first'), 2),
159
- makeEvent('assistant/message', assistantMessageData('step-a', 0, 1, 'second'), 4),
160
- makeEvent('assistant/message', assistantMessageData('step-m', 0, 2, 'third'), 6),
161
- makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 7),
162
- ]);
163
- const assistants = result.filter(message => message.kind === 'assistant');
164
- expect(assistants.map(message => message.id)).toEqual(['step-z', 'step-a', 'step-m']);
165
- expect(assistants.map(message => message.seq)).toEqual([2, 4, 6]);
166
- expect(assistants.every(message => message.pending !== true)).toBe(true);
167
- });
168
- it('keeps stable insertion order when legacy rows share a seq', () => {
169
- const folder = new EventFolder([
170
- { id: 'z-last-lexically', kind: 'assistant', text: 'first', seq: 5, time: 5_000 },
171
- { id: 'a-first-lexically', kind: 'assistant', text: 'second', seq: 5, time: 5_000 },
172
- ]);
173
- expect(folder.snapshot().map(message => message.id)).toEqual(['z-last-lexically', 'a-first-lexically']);
174
- });
175
- it('is idempotent: applying the same batch twice yields an identical list', () => {
176
- const events = [
177
- makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
178
- textChunk(0, 0, '一', 1),
179
- textChunk(0, 0, '二', 2),
180
- makeEvent('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, 3),
181
- makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '一二'), 4),
182
- makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 5),
183
- ];
184
- const once = foldEvents(events);
185
- const twice = foldEvents(events, once);
186
- expect(twice).toEqual(once);
187
- expect(twice.filter(message => message.kind === 'assistant')).toHaveLength(1);
188
- // Text must not have doubled from re-aggregating the streamed chunks.
189
- expect(twice.find(message => message.kind === 'assistant')?.text).toBe('一二');
190
- });
191
- it('skips unknown / unsupported event types safely', () => {
192
- const events = [
193
- makeEvent('session/end-seed', {}, 0),
194
- makeEvent('turn/start', { turn: 0 }, 1),
195
- makeEvent('user/message', userMessageData('u-1', 'hello'), 2),
196
- makeEvent('some/future-plugin', { whatever: true }, 3),
197
- makeEvent('goal/change', { objective: 'x' }, 4),
198
- ];
199
- const result = foldEvents(events);
200
- expect(result).toHaveLength(1);
201
- expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user' });
202
- });
203
- it('does not mutate the caller-supplied existing list', () => {
204
- const first = foldEvents([makeEvent('user/message', userMessageData('u-1', 'hello'), 0)]);
205
- const snapshot = JSON.stringify(first);
206
- foldEvents([makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, 'world'), 1)], first);
207
- expect(JSON.stringify(first)).toBe(snapshot);
208
- });
209
- it('request/context sets the context window used by a later assistant/message with usage', () => {
210
- const events = [
211
- makeEvent('request/context', { provider: 'fx', model: 'fx-1', contextWindow: 100_000 }, 0),
212
- makeEvent('user/message', userMessageData('u-1', 'hi'), 1),
213
- makeEvent('assistant/message', {
214
- turn: 0,
215
- step: 0,
216
- message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
217
- usage: { inputTokens: 10, outputTokens: 5 },
218
- }, 2),
219
- ];
220
- const result = foldEvents(events);
221
- const assistant = result.find(message => message.kind === 'assistant');
222
- expect(assistant).toMatchObject({
223
- usage: { inputTokens: 10, outputTokens: 5 },
224
- contextWindow: 100_000,
225
- });
226
- });
227
- it('usage without a context window carries no contextWindow', () => {
228
- const events = [
229
- makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
230
- makeEvent('assistant/message', {
231
- turn: 0,
232
- step: 0,
233
- message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
234
- usage: { inputTokens: 10, outputTokens: 5 },
235
- }, 1),
236
- ];
237
- const result = foldEvents(events);
238
- const assistant = result.find(message => message.kind === 'assistant');
239
- expect(assistant).toMatchObject({ usage: { inputTokens: 10, outputTokens: 5 } });
240
- expect(assistant?.contextWindow).toBeUndefined();
241
- });
242
- it('usage and contextWindow are both attached when request/context precedes the message', () => {
243
- const events = [
244
- makeEvent('request/context', { provider: 'fx', model: 'fx-1', contextWindow: 200_000 }, 0),
245
- makeEvent('user/message', userMessageData('u-1', 'hi'), 1),
246
- makeEvent('assistant/message', {
247
- turn: 0,
248
- step: 0,
249
- message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
250
- usage: { inputTokens: 30, outputTokens: 2, cacheReadTokens: 8, cacheWriteTokens: 1 },
251
- }, 2),
252
- ];
253
- const result = foldEvents(events);
254
- const assistant = result.find(message => message.kind === 'assistant');
255
- expect(assistant).toMatchObject({
256
- usage: { inputTokens: 30, outputTokens: 2, cacheReadTokens: 8, cacheWriteTokens: 1 },
257
- contextWindow: 200_000,
258
- });
259
- });
260
- it('assistant/message without usage has no usage field', () => {
261
- const events = [
262
- makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
263
- makeEvent('assistant/message', { turn: 0, step: 0, message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: 'ok' }] } }, 1),
264
- ];
265
- const result = foldEvents(events);
266
- const assistant = result.find(message => message.kind === 'assistant');
267
- expect(assistant?.usage).toBeUndefined();
268
- expect(assistant?.contextWindow).toBeUndefined();
269
- });
270
- it('attaches sourceKind from source.kind for a user message', () => {
271
- const events = [
272
- makeEvent('user/message', {
273
- id: 'u-plugin',
274
- role: 'user',
275
- content: [{ type: 'text', text: '系统注入' }],
276
- source: { kind: 'plugin', name: 'react-extension' },
277
- }, 0),
278
- ];
279
- const result = foldEvents(events);
280
- expect(result[0]).toMatchObject({ id: 'u-plugin', kind: 'user', sourceKind: 'plugin' });
281
- });
282
- it('keeps sourceKind on a replayed/replaced user/message', () => {
283
- const first = foldEvents([makeEvent('user/message', {
284
- id: 'u-1',
285
- role: 'user',
286
- content: [{ type: 'text', text: '第一版' }],
287
- source: { kind: 'plugin' },
288
- }, 0)]);
289
- const second = foldEvents([makeEvent('user/message', {
290
- id: 'u-1',
291
- role: 'user',
292
- content: [{ type: 'text', text: '第二版' }],
293
- source: { kind: 'plugin' },
294
- }, 1)], first);
295
- expect(second[0]).toMatchObject({ id: 'u-1', text: '第二版', sourceKind: 'plugin' });
296
- });
297
- it('request/context and unknown events are still ignored by the fold', () => {
298
- const events = [
299
- makeEvent('some/future-event', { nope: true }, 0),
300
- makeEvent('request/context', { provider: 'fx', model: 'fx-1' }, 1),
301
- makeEvent('user/message', userMessageData('u-1', '真实消息'), 2),
302
- ];
303
- const result = foldEvents(events);
304
- // request/context with no window and the unknown event render nothing.
305
- expect(result).toHaveLength(1);
306
- expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user' });
307
- });
308
- });
309
- describe('EventFolder incremental folding', () => {
310
- it('matches a one-shot fold when events arrive one at a time', () => {
311
- const stream = [
312
- makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
313
- textChunk(0, 0, '你', 1),
314
- textChunk(0, 0, '好', 2),
315
- makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '你好'), 3),
316
- makeEvent('message/update', { id: 'u-1', text: '更新' }, 4),
317
- ];
318
- const oneShot = foldEvents(stream);
319
- const folder = new EventFolder();
320
- let incremental = [];
321
- for (const event of stream)
322
- incremental = folder.fold([event]);
323
- expect(incremental).toEqual(oneShot);
324
- });
325
- it('replays and re-folds are no-ops and reuse the previous snapshot identity', () => {
326
- const folder = new EventFolder(foldEvents([
327
- makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
328
- textChunk(0, 0, '你', 1),
329
- ]));
330
- const first = folder.fold([textChunk(0, 0, '好', 2)]);
331
- expect(first[first.length - 1]).toMatchObject({ text: '你好', pending: true });
332
- // Same event again (wire replay or a double-invoked state updater): no change, same identity.
333
- const replay = folder.fold([textChunk(0, 0, '好', 2)]);
334
- expect(replay).toBe(first);
335
- // A no-op batch over an already folded stream also keeps the identity.
336
- expect(folder.fold([])).toBe(first);
337
- });
338
- it('prepends older pages and keeps folding live events on top', () => {
339
- const folder = new EventFolder(foldEvents([
340
- makeEvent('user/message', userMessageData('u-2', '第二页'), 10),
341
- ]));
342
- folder.prepend(foldEvents([makeEvent('user/message', userMessageData('u-1', '第一页'), 5)]));
343
- const withLive = folder.fold([textChunk(0, 0, '新', 11)]);
344
- expect(withLive.map(message => message.id)).toEqual(['u-1', 'u-2', 'assistant,0.0#11']);
345
- // The live fold must not lose the prepended rows on later events.
346
- const later = folder.fold([textChunk(0, 0, '续', 12)]);
347
- expect(later.map(message => message.id)).toEqual(['u-1', 'u-2', 'assistant,0.0#11']);
348
- expect(later[2]).toMatchObject({ text: '新续', pending: true });
349
- });
350
- it('seed replaces the whole stream', () => {
351
- const folder = new EventFolder(foldEvents([makeEvent('user/message', userMessageData('u-1', '旧'), 0)]));
352
- folder.seed(foldEvents([makeEvent('user/message', userMessageData('u-2', '新'), 5)]));
353
- expect(folder.snapshot().map(message => message.id)).toEqual(['u-2']);
354
- expect(folder.fold([textChunk(0, 0, '追加', 6)]).map(message => message.id)).toEqual(['u-2', 'assistant,0.0#6']);
355
- });
356
- });
@@ -1,6 +0,0 @@
1
- /**
2
- * Mobile surface stylesheet, shipped as a string and injected at boot (the
3
- * standalone page has no CSS-module pipeline).
4
- */
5
- export declare const mobileCss = "/* Mobile surface chrome: standalone stylesheet (no main-UI design tokens \u2014\n this page boots without the shell). Light-first palette; the dark palette\n applies only when the header toggle sets [data-theme='dark']. */\n\n:root {\n --m-bg: #f3f5f9;\n --m-bg-raised: #ffffff;\n --m-bg-input: #ffffff;\n --m-border: #dfe4ec;\n --m-text: #1c2333;\n --m-text-secondary: #4d5768;\n --m-text-tertiary: #8b95a7;\n --m-accent: #2f6fed;\n --m-accent-soft: rgba(47, 111, 237, 0.12);\n --m-danger: #d64545;\n --m-success: #1e9e6a;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);\n --m-shadow-raise: 0 6px 20px rgba(16, 24, 40, 0.1);\n --m-backdrop: rgba(15, 23, 42, 0.4);\n}\n\n:root[data-theme='dark'] {\n --m-bg: #111418;\n --m-bg-raised: #1a1f26;\n --m-bg-input: #20262e;\n --m-border: #2b333d;\n --m-text: #e8eaed;\n --m-text-secondary: #9aa3ad;\n --m-text-tertiary: #6b747f;\n --m-accent: #4f8cff;\n --m-accent-soft: rgba(79, 140, 255, 0.14);\n --m-danger: #e5534b;\n --m-success: #3fb68b;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n --m-shadow-raise: 0 6px 20px rgba(0, 0, 0, 0.4);\n --m-backdrop: rgba(0, 0, 0, 0.55);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n height: 100%;\n background: var(--m-bg);\n color: var(--m-text);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC', 'Segoe UI', sans-serif;\n font-size: 15px;\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n}\n\n/* The app fills exactly one viewport: the page itself never scrolls, each\n view owns its scroll region (chat history, session lists) and the chat\n composer stays pinned to the bottom of the screen. */\n#root {\n height: 100dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.mobile {\n min-height: 0;\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* \u2500\u2500 header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-header {\n position: sticky;\n top: 0;\n z-index: 10;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: calc(env(safe-area-inset-top, 0px) + 12px) 16px 10px;\n background: color-mix(in srgb, var(--m-bg) 88%, transparent);\n backdrop-filter: blur(10px);\n border-bottom: 1px solid var(--m-border);\n}\n\n.mobile-title {\n margin: 0;\n font-size: 17px;\n font-weight: 600;\n}\n\n.mobile-titleInline {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mobile-back {\n flex: none;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text);\n font-size: 22px;\n line-height: 1;\n cursor: pointer;\n transition: background-color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-back:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-back:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle {\n flex: none;\n margin-left: auto;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text-secondary);\n cursor: pointer;\n transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-theme-toggle:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-theme-toggle:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle svg {\n width: 18px;\n height: 18px;\n}\n\n/* \u2500\u2500 lists \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n list-style: none;\n margin: 0;\n padding: 8px 0 calc(env(safe-area-inset-bottom, 0px) + 16px);\n}\n\n.mobile-row {\n display: flex;\n align-items: center;\n gap: 10px;\n width: calc(100% - 24px);\n margin: 0 12px 8px;\n padding: 13px 14px;\n border: 1px solid var(--m-border);\n border-radius: 14px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.mobile-row:active {\n background: color-mix(in srgb, var(--m-text) 5%, var(--m-bg-raised));\n}\n\n.mobile-rowMain {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.mobile-rowTitle {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 15px;\n}\n\n.mobile-rowMeta {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.mobile-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 18px;\n}\n\n.mobile-live {\n margin-left: 6px;\n color: var(--m-success);\n font-size: 10px;\n vertical-align: middle;\n}\n\n/* \u2500\u2500 empty / error states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px;\n}\n\n.mobile-muted {\n margin: 0;\n color: var(--m-text-tertiary);\n}\n\n.mobile-error {\n margin: 0;\n color: var(--m-danger);\n}\n\n.mobile-pad {\n padding: 8px 16px;\n}\n\n.mobile-create {\n display: grid;\n gap: 8px;\n}\n\n.mobile-preset {\n display: grid;\n gap: 4px;\n}\n\n.mobile-presetLabel {\n color: var(--m-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.mobile-presetSelect {\n width: 100%;\n height: 40px;\n padding: 0 36px 0 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n}\n\n.mobile-presetSelect:disabled {\n opacity: 0.6;\n}\n\n.mobile-presetDescription {\n margin: 0;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-button {\n height: 38px;\n padding: 0 18px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.mobile-button:disabled {\n opacity: 0.5;\n}\n\n.mobile-block {\n display: block;\n width: calc(100% - 32px);\n margin: 4px 16px;\n}\n\n/* Primary action (create session): accent fill, readable on both themes. */\n.mobile-new {\n display: block;\n width: 100%;\n height: 44px;\n border: none;\n border-radius: 12px;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(47, 111, 237, 0.3);\n}\n\n.mobile-new:active {\n opacity: 0.88;\n}\n\n.mobile-new:disabled {\n opacity: 0.6;\n}\n\n.mobile-hint {\n display: block;\n margin-top: 2px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 chat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.chat-scroll {\n flex: 1;\n overflow-y: auto;\n padding: 12px 14px calc(env(safe-area-inset-bottom, 0px) + 12px);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-msg {\n max-width: 88%;\n padding: 10px 12px;\n border-radius: var(--m-radius);\n font-size: 14.5px;\n line-height: 1.55;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.chat-msg-user {\n align-self: flex-end;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n border-bottom-right-radius: 4px;\n box-shadow: 0 1px 3px rgba(47, 111, 237, 0.28);\n}\n\n.chat-msg-assistant {\n align-self: flex-start;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom-left-radius: 4px;\n box-shadow: var(--m-shadow);\n}\n\n.chat-msg-pending .chat-msg-text::after {\n content: '\u258D';\n animation: chat-blink 1s steps(2) infinite;\n}\n\n@keyframes chat-blink {\n 50% {\n opacity: 0;\n }\n}\n\n.chat-msg-failed {\n border-color: var(--m-danger);\n}\n\n.chat-msg-failtag {\n display: inline-block;\n margin-top: 6px;\n padding: 1px 8px;\n border: 1px solid var(--m-danger);\n border-radius: 999px;\n color: var(--m-danger);\n font-size: 11px;\n}\n\n.chat-msg-time {\n display: block;\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 10.5px;\n line-height: 1.2;\n}\n\n.chat-msg-toggle {\n display: block;\n margin-top: 8px;\n padding: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-accent);\n font-size: 13px;\n cursor: pointer;\n}\n\n/* Assistant markdown content (GFM subset; renderer in markdown.ts). */\n.chat-md {\n white-space: normal;\n}\n.chat-md-collapsed {\n max-height: 45vh;\n overflow: hidden;\n position: relative;\n}\n.chat-md-collapsed::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 28px;\n background: linear-gradient(transparent, var(--m-bg-raised));\n pointer-events: none;\n}\n.chat-md-body p { margin: 0 0 8px; }\n.chat-md-body p:last-child { margin-bottom: 0; }\n.chat-md-body h1, .chat-md-body h2, .chat-md-body h3,\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 {\n margin: 12px 0 6px;\n font-weight: 650;\n line-height: 1.3;\n}\n.chat-md-body h1 { font-size: 1.35em; }\n.chat-md-body h2 { font-size: 1.25em; }\n.chat-md-body h3 { font-size: 1.15em; }\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 { font-size: 1.05em; }\n.chat-md-body code {\n font-family: var(--m-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: 0.9em;\n background: var(--m-accent-soft);\n padding: 1px 5px;\n border-radius: 4px;\n}\n.chat-md-body pre {\n margin: 8px 0;\n padding: 10px;\n background: #101726;\n color: #dbe4f5;\n border-radius: 8px;\n overflow-x: auto;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.chat-md-body pre code {\n background: transparent;\n padding: 0;\n color: inherit;\n}\n.chat-md-body ul, .chat-md-body ol { margin: 4px 0 8px; padding-left: 22px; }\n.chat-md-body li { margin: 2px 0; }\n.chat-md-body blockquote {\n margin: 8px 0;\n padding: 4px 10px;\n border-left: 3px solid var(--m-accent);\n background: var(--m-accent-soft);\n border-radius: 0 6px 6px 0;\n color: var(--m-text-secondary);\n}\n.chat-md-body table {\n margin: 8px 0;\n border-collapse: collapse;\n display: block;\n overflow-x: auto;\n font-size: 13px;\n}\n.chat-md-body th, .chat-md-body td {\n border: 1px solid var(--m-border);\n padding: 4px 8px;\n}\n.chat-md-body th { background: var(--m-accent-soft); }\n.chat-md-body a { color: var(--m-accent); }\n.chat-md-body hr { border: none; border-top: 1px solid var(--m-border); margin: 10px 0; }\n.chat-md-body img { max-width: 100%; border-radius: 8px; }\n\n/* \u2500\u2500 message disclosures (reasoning / tools) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-disclosure {\n margin-bottom: 8px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: color-mix(in srgb, var(--m-bg) 55%, transparent);\n overflow: hidden;\n}\n\n.chat-disclosure-head {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 7px 10px;\n border: none;\n background: transparent;\n color: var(--m-text);\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.chat-disclosure-caret {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n line-height: 1;\n transition: transform 0.15s ease;\n}\n\n.chat-disclosure-open .chat-disclosure-caret {\n transform: rotate(90deg);\n}\n\n.chat-disclosure-label {\n flex: none;\n color: var(--m-text-secondary);\n}\n\n.chat-reasoning[data-pending] .chat-disclosure-label {\n color: var(--m-accent);\n}\n\n.chat-disclosure-summary {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-disclosure-count {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-disclosure-body {\n padding: 8px 10px 10px;\n border-top: 1px dashed var(--m-border);\n color: var(--m-text-secondary);\n font-size: 12.5px;\n line-height: 1.6;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n max-height: 40vh;\n overflow-y: auto;\n}\n\n.chat-tools-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.chat-tool-card {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 6px 8px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-raised);\n}\n\n.chat-tool-pills {\n display: flex;\n gap: 4px;\n flex-wrap: wrap;\n align-items: center;\n}\n\n.chat-tool-pill {\n display: inline-block;\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--m-accent-soft);\n color: var(--m-accent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n font-weight: 600;\n white-space: nowrap;\n}\n\n.chat-tool-args {\n margin: 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--m-bg-input);\n color: var(--m-text-secondary);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n max-height: 200px;\n overflow-y: auto;\n}\n\n/* \u2500\u2500 turn status indicator (#1017) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-turn-status {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 10px 16px;\n color: var(--m-accent);\n font-size: 13px;\n font-weight: 600;\n background: linear-gradient(\n 90deg,\n var(--m-accent) 0%,\n color-mix(in srgb, var(--m-accent) 60%, #ffffff) 50%,\n var(--m-accent) 100%\n );\n background-size: 200% 100%;\n background-clip: text;\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n animation: dsh-shimmer 1.8s linear infinite;\n}\n\n.chat-turn-dots {\n display: inline-flex;\n gap: 3px;\n}\n\n.chat-turn-dots span {\n display: inline-block;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--m-accent);\n animation: dsh-dot-bounce 1.2s ease-in-out infinite;\n}\n\n.chat-turn-dots span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.chat-turn-dots span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dsh-shimmer {\n from { background-position: 200% 0; }\n to { background-position: -200% 0; }\n}\n\n@keyframes dsh-dot-bounce {\n 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }\n 40% { transform: translateY(-4px); opacity: 1; }\n}\n\n/* \u2500\u2500 approval panel (#1025) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-approval-panel {\n margin: 8px 12px;\n padding: 10px 12px;\n border: 1px solid var(--m-accent);\n border-radius: var(--m-radius);\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n}\n\n.chat-approval-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 8px;\n}\n\n.chat-approval-reason {\n color: var(--m-text-secondary);\n font-size: 12.5px;\n}\n\n.chat-approval-error {\n margin: 4px 0;\n color: var(--m-danger);\n font-size: 12px;\n}\n\n.chat-approval-actions {\n display: flex;\n gap: 8px;\n}\n\n.chat-approval-allow {\n flex: 1;\n height: 34px;\n border: none;\n border-radius: 8px;\n background: var(--m-accent);\n color: #fff;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.chat-approval-allow:disabled {\n opacity: 0.6;\n}\n\n.chat-approval-reject {\n flex: 1;\n height: 34px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n\n.chat-approval-reject:disabled {\n opacity: 0.6;\n}\n\n/* \u2500\u2500 question panel (#1025) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-question-panel {\n margin: 8px 12px;\n padding: 10px 12px;\n border: 1px solid var(--m-accent);\n border-radius: var(--m-radius);\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-question-group {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.chat-question-header {\n font-size: 14px;\n font-weight: 600;\n color: var(--m-text);\n}\n\n.chat-question-text {\n font-size: 13px;\n color: var(--m-text);\n line-height: 1.5;\n}\n\n.chat-question-detail {\n font-size: 12px;\n color: var(--m-text-secondary);\n line-height: 1.5;\n}\n\n.chat-question-options {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.chat-question-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n cursor: pointer;\n font-size: 13px;\n transition: border-color 0.12s, background 0.12s;\n}\n\n.chat-question-option input {\n margin: 2px 0 0;\n flex: none;\n}\n\n.chat-question-option-selected {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-question-option-label {\n color: var(--m-text);\n font-weight: 500;\n}\n\n.chat-question-option-desc {\n display: block;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-question-custom {\n width: 100%;\n min-height: 40px;\n box-sizing: border-box;\n padding: 8px 10px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n font-size: 13px;\n resize: vertical;\n outline: none;\n}\n\n.chat-question-custom:focus-visible {\n border-color: var(--m-accent);\n}\n\n.chat-question-submit {\n height: 36px;\n border: none;\n border-radius: 8px;\n background: var(--m-accent);\n color: #fff;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.chat-question-submit:disabled {\n opacity: 0.6;\n}\n\n\n/* \u2500\u2500 composer toolbar (model / permission chips) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-tools {\n display: flex;\n gap: 8px;\n padding: 6px 12px 0;\n}\n\n.chat-chip {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n font-size: 12px;\n cursor: pointer;\n}\n\n.chat-chip:active {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-chip-label {\n flex: none;\n color: var(--m-text-tertiary);\n}\n\n.chat-chip-value {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.chat-chip-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n}\n\n/* \u2500\u2500 bottom sheets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-backdrop {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n background: var(--m-backdrop);\n animation: sheet-fade 0.18s ease;\n}\n\n@keyframes sheet-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.sheet {\n max-height: 74dvh;\n display: flex;\n flex-direction: column;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom: none;\n border-radius: 16px 16px 0 0;\n box-shadow: var(--m-shadow-raise);\n padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);\n animation: sheet-up 0.22s ease;\n}\n\n@keyframes sheet-up {\n from {\n transform: translateY(40px);\n opacity: 0.6;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n.sheet-handle {\n flex: none;\n align-self: center;\n width: 36px;\n height: 4px;\n margin: 8px 0 4px;\n border-radius: 999px;\n background: var(--m-border);\n}\n\n.sheet-title {\n flex: none;\n padding: 6px 16px 10px;\n font-size: 15px;\n font-weight: 600;\n}\n\n.sheet-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 12px 8px;\n}\n\n.sheet-status {\n padding: 18px 8px;\n color: var(--m-text-tertiary);\n font-size: 13px;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n}\n\n.sheet-status-error {\n color: var(--m-danger);\n}\n\n.sheet-hint {\n margin: 0;\n color: var(--m-text-secondary);\n font-size: 12px;\n line-height: 1.5;\n max-width: 32em;\n}\n\n.sheet-error {\n margin: 0 0 8px;\n color: var(--m-danger);\n font-size: 12.5px;\n}\n\n.sheet-section {\n margin-bottom: 14px;\n}\n\n.sheet-section-title {\n padding: 4px 8px 6px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.sheet-option {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 10px 12px;\n border: 1px solid transparent;\n border-radius: 10px;\n background: transparent;\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.sheet-option:active {\n background: var(--m-bg-input);\n}\n\n.sheet-option-selected {\n border-color: var(--m-accent);\n background: color-mix(in srgb, var(--m-accent) 12%, transparent);\n}\n\n.sheet-option:disabled {\n opacity: 0.5;\n}\n\n.sheet-option-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-option-title {\n font-size: 14px;\n}\n\n.sheet-option-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-option-check {\n flex: none;\n color: var(--m-accent);\n font-size: 15px;\n font-weight: 700;\n}\n\n.sheet-confirm-desc {\n margin: 0 4px 14px;\n color: var(--m-text-secondary);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.sheet-confirm-actions {\n display: flex;\n gap: 10px;\n padding: 4px;\n}\n\n.sheet-confirm-actions .mobile-button {\n flex: 1;\n}\n\n.sheet-confirm-danger {\n flex: 1;\n height: 38px;\n border: none;\n border-radius: 10px;\n background: var(--m-danger);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.sheet-confirm-danger:disabled {\n opacity: 0.5;\n}\n\n.chat-meta {\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-typing {\n color: var(--m-text-tertiary);\n font-size: 13px;\n padding: 4px 2px;\n}\n\n.chat-inputbar {\n display: flex;\n gap: 8px;\n padding: 10px 12px calc(env(safe-area-inset-bottom, 0px) + 10px);\n border-top: 1px solid var(--m-border);\n background: var(--m-bg);\n}\n\n.chat-input {\n flex: 1;\n min-width: 0;\n min-height: 40px;\n max-height: 120px;\n /* Grow with the content (still bounded by min/max-height); browsers\n without field-sizing support ignore this line and keep the old size. */\n field-sizing: content;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n font-size: 14.5px;\n resize: none;\n outline: none;\n}\n\n.chat-input:focus {\n border-color: var(--m-accent);\n}\n\n.chat-send {\n flex: none;\n align-self: flex-end;\n height: 40px;\n padding: 0 16px;\n border: none;\n border-radius: 10px;\n background: var(--m-accent);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.chat-send:disabled {\n opacity: 0.5;\n}\n\n/* Running state: the primary button carries only the square stop icon, so\n it centers the glyph instead of sizing to text. */\n.chat-send-stop {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 44px;\n padding: 0 12px;\n}\n\n.chat-load-older {\n align-self: center;\n margin: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-text-secondary);\n font-size: 13px;\n cursor: pointer;\n padding: 6px 10px;\n}\n\n/* Touch surfaces have no hover, so keyboard focus must be fully visible.\n One shared ring keeps every interactive control consistent and readable\n on both palettes. */\n.mobile-row:focus-visible,\n.mobile-button:focus-visible,\n.mobile-new:focus-visible,\n.mobile-presetSelect:focus-visible,\n.chat-send:focus-visible,\n.chat-chip:focus-visible,\n.chat-msg-toggle:focus-visible,\n.chat-load-older:focus-visible,\n.chat-disclosure-head:focus-visible,\n.sheet-option:focus-visible,\n.sheet-confirm-danger:focus-visible,\n.sheet-toggle-switch:focus-visible,\n.chat-input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n/* \u2500\u2500 chat context usage chip (chat-tools row) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-context {\n flex: none;\n align-self: center;\n padding: 2px 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: color-mix(in srgb, var(--m-text-tertiary) 10%, transparent);\n color: var(--m-text-tertiary);\n font-size: 11px;\n line-height: 1.6;\n white-space: nowrap;\n}\n\n.chat-context-warn {\n border-color: var(--m-danger);\n background: color-mix(in srgb, var(--m-danger) 14%, transparent);\n color: var(--m-danger);\n}\n\n/* \u2500\u2500 display sheet toggle rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-toggle-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 8px;\n border-bottom: 1px solid var(--m-border);\n}\n\n.sheet-toggle-row:last-child {\n border-bottom: none;\n}\n\n.sheet-toggle-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-toggle-title {\n font-size: 14px;\n}\n\n.sheet-toggle-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-toggle-switch {\n position: relative;\n flex: none;\n width: 44px;\n height: 26px;\n border: none;\n border-radius: 999px;\n background: var(--m-border);\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.sheet-toggle-switch-on {\n background: var(--m-accent);\n}\n\n.sheet-toggle-switch-knob {\n position: absolute;\n top: 3px;\n left: 3px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 2px rgba(16, 24, 40, 0.2);\n transition: transform 0.15s ease;\n}\n\n.sheet-toggle-switch-on .sheet-toggle-switch-knob {\n transform: translateX(18px);\n}\n\n/* Accessibility: collapse every interaction/enter animation for\n reduced-motion users; surface layout itself stays static. */\n@media (prefers-reduced-motion: reduce) {\n .mobile-theme-toggle,\n .mobile-back,\n .mobile-row,\n .mobile-button,\n .mobile-new,\n .chat-send,\n .chat-chip,\n .chat-msg-toggle,\n .chat-load-older,\n .chat-disclosure-head,\n .chat-disclosure-caret,\n .chat-msg-pending .chat-msg-text::after,\n .sheet-backdrop,\n .sheet,\n .sheet-option,\n .sheet-confirm-danger,\n .sheet-toggle-switch,\n .sheet-toggle-switch-knob,\n .chat-input,\n .chat-turn-status,\n .chat-turn-dots span {\n animation: none;\n transition: none;\n }\n .chat-turn-status {\n background: none;\n -webkit-text-fill-color: var(--m-accent);\n }\n}\n\n/* \u2500\u2500 installed-app pairing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-pair {\n min-height: 100dvh;\n display: grid;\n place-items: center;\n padding: 24px;\n}\n\n.mobile-pairCard {\n display: grid;\n width: min(100%, 420px);\n gap: 12px;\n padding: 24px;\n border: 1px solid var(--m-border);\n border-radius: 12px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow-raise);\n}\n\n.mobile-pairLabel {\n color: var(--m-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.mobile-pairInput {\n width: 100%;\n min-height: 42px;\n box-sizing: border-box;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n outline: none;\n}\n\n.mobile-pairInput:focus-visible {\n border-color: var(--m-accent);\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-pairSubmit {\n width: 100%;\n}\n\n/* \u2500\u2500 directory browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.dir-browser {\n display: flex;\n flex-direction: column;\n}\n\n.dir-crumbs {\n display: flex;\n align-items: center;\n overflow-x: auto;\n padding: 12px 16px;\n background: var(--m-bg-raised);\n border-bottom: 1px solid var(--m-border);\n white-space: nowrap;\n}\n\n.dir-crumbs::-webkit-scrollbar {\n display: none;\n}\n\n.dir-crumb {\n border: none;\n background: transparent;\n padding: 4px 6px;\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.dir-crumb:active {\n background: var(--m-bg-input);\n border-radius: 4px;\n}\n\n.dir-crumb-separator {\n color: var(--m-text-tertiary);\n margin: 0 4px;\n font-size: 14px;\n}\n\n.dir-entry {\n padding-left: 16px;\n}\n\n.dir-entry-hidden {\n opacity: 0.5;\n}\n\n.dir-select {\n padding: 16px;\n background: var(--m-bg);\n border-top: 1px solid var(--m-border);\n}\n\n.dir-empty {\n padding: 40px 0;\n}\n";
6
- //# sourceMappingURL=mobile-styles.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mobile-styles.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,SAAS,w4/BAy5CrB,CAAA"}