@mooncompany/uplink-chat 0.5.0

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.

Potentially problematic release.


This version of @mooncompany/uplink-chat might be problematic. Click here for more details.

Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/bin/uplink.js +279 -0
  4. package/middleware/error-handler.js +69 -0
  5. package/package.json +93 -0
  6. package/public/css/agents.36b98c0f.css +1469 -0
  7. package/public/css/agents.css +1469 -0
  8. package/public/css/app.a6a7f8f5.css +2731 -0
  9. package/public/css/app.css +2731 -0
  10. package/public/css/artifacts.css +444 -0
  11. package/public/css/commands.css +55 -0
  12. package/public/css/connection.css +131 -0
  13. package/public/css/dashboard.css +233 -0
  14. package/public/css/developer.css +328 -0
  15. package/public/css/files.css +123 -0
  16. package/public/css/markdown.css +156 -0
  17. package/public/css/message-actions.css +278 -0
  18. package/public/css/mobile.css +614 -0
  19. package/public/css/panels-unified.css +483 -0
  20. package/public/css/premium.css +415 -0
  21. package/public/css/realtime.css +189 -0
  22. package/public/css/satellites.css +401 -0
  23. package/public/css/shortcuts.css +185 -0
  24. package/public/css/split-view.4def0262.css +673 -0
  25. package/public/css/split-view.css +673 -0
  26. package/public/css/theme-generator.css +391 -0
  27. package/public/css/themes.css +387 -0
  28. package/public/css/timestamps.css +54 -0
  29. package/public/css/variables.css +78 -0
  30. package/public/dist/bundle.b55050c4.js +15757 -0
  31. package/public/favicon.svg +24 -0
  32. package/public/img/agents/ada.png +0 -0
  33. package/public/img/agents/clarice.png +0 -0
  34. package/public/img/agents/dennis-nedry.png +0 -0
  35. package/public/img/agents/elliot-alderson.png +0 -0
  36. package/public/img/agents/main.png +0 -0
  37. package/public/img/agents/scotty.png +0 -0
  38. package/public/img/agents/top-flight-security.png +0 -0
  39. package/public/index.html +1083 -0
  40. package/public/js/agents-data.js +234 -0
  41. package/public/js/agents-ui.js +72 -0
  42. package/public/js/agents.js +1525 -0
  43. package/public/js/app.js +79 -0
  44. package/public/js/appearance-settings.js +111 -0
  45. package/public/js/artifacts.js +432 -0
  46. package/public/js/audio-queue.js +168 -0
  47. package/public/js/bootstrap.js +54 -0
  48. package/public/js/chat.js +1211 -0
  49. package/public/js/commands.js +581 -0
  50. package/public/js/connection-api.js +121 -0
  51. package/public/js/connection.js +1231 -0
  52. package/public/js/context-tracker.js +271 -0
  53. package/public/js/core.js +172 -0
  54. package/public/js/dashboard.js +452 -0
  55. package/public/js/developer.js +432 -0
  56. package/public/js/encryption.js +124 -0
  57. package/public/js/errors.js +122 -0
  58. package/public/js/event-bus.js +77 -0
  59. package/public/js/fetch-utils.js +171 -0
  60. package/public/js/file-handler.js +229 -0
  61. package/public/js/files.js +352 -0
  62. package/public/js/gateway-chat.js +538 -0
  63. package/public/js/logger.js +112 -0
  64. package/public/js/markdown.js +190 -0
  65. package/public/js/message-actions.js +431 -0
  66. package/public/js/message-renderer.js +288 -0
  67. package/public/js/missed-messages.js +235 -0
  68. package/public/js/mobile-debug.js +95 -0
  69. package/public/js/notifications.js +367 -0
  70. package/public/js/offline-queue.js +178 -0
  71. package/public/js/onboarding.js +543 -0
  72. package/public/js/panels.js +156 -0
  73. package/public/js/premium.js +412 -0
  74. package/public/js/realtime-voice.js +844 -0
  75. package/public/js/satellite-sync.js +256 -0
  76. package/public/js/satellite-ui.js +175 -0
  77. package/public/js/satellites.js +1516 -0
  78. package/public/js/settings.js +1087 -0
  79. package/public/js/shortcuts.js +381 -0
  80. package/public/js/split-chat.js +1234 -0
  81. package/public/js/split-resize.js +211 -0
  82. package/public/js/splitview.js +340 -0
  83. package/public/js/storage.js +408 -0
  84. package/public/js/streaming-handler.js +324 -0
  85. package/public/js/stt-settings.js +316 -0
  86. package/public/js/theme-generator.js +661 -0
  87. package/public/js/themes.js +164 -0
  88. package/public/js/timestamps.js +198 -0
  89. package/public/js/tts-settings.js +575 -0
  90. package/public/js/ui.js +267 -0
  91. package/public/js/update-notifier.js +143 -0
  92. package/public/js/utils/constants.js +165 -0
  93. package/public/js/utils/sanitize.js +93 -0
  94. package/public/js/utils/sse-parser.js +195 -0
  95. package/public/js/voice.js +883 -0
  96. package/public/manifest.json +58 -0
  97. package/public/moon_texture.jpg +0 -0
  98. package/public/sw.js +221 -0
  99. package/public/three.min.js +6 -0
  100. package/server/channel.js +529 -0
  101. package/server/chat.js +270 -0
  102. package/server/config-store.js +362 -0
  103. package/server/config.js +159 -0
  104. package/server/context.js +131 -0
  105. package/server/gateway-commands.js +211 -0
  106. package/server/gateway-proxy.js +318 -0
  107. package/server/index.js +22 -0
  108. package/server/logger.js +89 -0
  109. package/server/middleware/auth.js +188 -0
  110. package/server/middleware.js +218 -0
  111. package/server/openclaw-discover.js +308 -0
  112. package/server/premium/index.js +156 -0
  113. package/server/premium/license.js +140 -0
  114. package/server/realtime/bridge.js +837 -0
  115. package/server/realtime/index.js +349 -0
  116. package/server/realtime/tts-stream.js +446 -0
  117. package/server/routes/agents.js +564 -0
  118. package/server/routes/artifacts.js +174 -0
  119. package/server/routes/chat.js +311 -0
  120. package/server/routes/config-settings.js +345 -0
  121. package/server/routes/config.js +603 -0
  122. package/server/routes/files.js +307 -0
  123. package/server/routes/index.js +18 -0
  124. package/server/routes/media.js +451 -0
  125. package/server/routes/missed-messages.js +107 -0
  126. package/server/routes/premium.js +75 -0
  127. package/server/routes/push.js +156 -0
  128. package/server/routes/satellite.js +406 -0
  129. package/server/routes/status.js +251 -0
  130. package/server/routes/stt.js +35 -0
  131. package/server/routes/voice.js +260 -0
  132. package/server/routes/webhooks.js +203 -0
  133. package/server/routes.js +206 -0
  134. package/server/runtime-config.js +336 -0
  135. package/server/share.js +305 -0
  136. package/server/stt/faster-whisper.js +72 -0
  137. package/server/stt/groq.js +51 -0
  138. package/server/stt/index.js +196 -0
  139. package/server/stt/openai.js +49 -0
  140. package/server/sync.js +244 -0
  141. package/server/tailscale-https.js +175 -0
  142. package/server/tts.js +646 -0
  143. package/server/update-checker.js +172 -0
  144. package/server/utils/filename.js +129 -0
  145. package/server/utils.js +147 -0
  146. package/server/watchdog.js +318 -0
  147. package/server/websocket/broadcast.js +359 -0
  148. package/server/websocket/connections.js +339 -0
  149. package/server/websocket/index.js +215 -0
  150. package/server/websocket/routing.js +277 -0
  151. package/server/websocket/sync.js +102 -0
  152. package/server.js +404 -0
  153. package/utils/detect-tool-usage.js +93 -0
  154. package/utils/errors.js +158 -0
  155. package/utils/html-escape.js +84 -0
  156. package/utils/id-sanitize.js +94 -0
  157. package/utils/response.js +130 -0
  158. package/utils/with-retry.js +105 -0
@@ -0,0 +1,673 @@
1
+ /**
2
+ * Split-View Dual Chat Layout
3
+ * Desktop-only (≥1024px) side-by-side chat sessions
4
+ * Splits the CHAT AREA inside .app, not the whole layout
5
+ */
6
+
7
+ /* ==========================================================================
8
+ Split View Variables
9
+ ========================================================================== */
10
+
11
+ :root {
12
+ --split-ratio: 0.5; /* Default 50/50 split, updated by drag */
13
+ --split-handle-width: 4px;
14
+ --split-min-width: 320px;
15
+ --split-transition: 300ms cubic-bezier(0.4, 0, 0.2, 1);
16
+ }
17
+
18
+ /* ==========================================================================
19
+ Split Chat Container
20
+ ========================================================================== */
21
+
22
+ /* Default: single column, primary takes all space */
23
+ .split-chat-container {
24
+ display: flex;
25
+ flex-direction: column;
26
+ flex: 1;
27
+ min-height: 0;
28
+ overflow: hidden;
29
+ }
30
+
31
+ /* Desktop: when split-view is active, switch to row layout */
32
+ @media (min-width: 1024px) {
33
+ body.split-view-active .split-chat-container {
34
+ flex-direction: row;
35
+ }
36
+ }
37
+
38
+ /* ==========================================================================
39
+ Primary Chat Pane
40
+ ========================================================================== */
41
+
42
+ .split-pane-primary {
43
+ display: flex;
44
+ flex-direction: column;
45
+ flex: 1;
46
+ min-height: 0;
47
+ min-width: 0;
48
+ overflow: hidden;
49
+ }
50
+
51
+ @media (min-width: 1024px) {
52
+ body.split-view-active .split-pane-primary {
53
+ flex: 0 0 calc(var(--split-ratio) * 100%);
54
+ min-width: var(--split-min-width);
55
+ }
56
+
57
+ /* Only animate on open/close, not during drag */
58
+ body.split-view-active:not(.split-dragging) .split-pane-primary {
59
+ transition: flex var(--split-transition);
60
+ }
61
+ }
62
+
63
+ /* ==========================================================================
64
+ Split View Button (Header)
65
+ ========================================================================== */
66
+
67
+ #splitViewBtn {
68
+ position: relative;
69
+ }
70
+
71
+ /* Glow effect when activated */
72
+ body.split-view-active #splitViewBtn {
73
+ color: var(--accent);
74
+ background: var(--accent-10);
75
+ }
76
+
77
+ body.split-view-active #splitViewBtn::after {
78
+ content: '';
79
+ position: absolute;
80
+ inset: -2px;
81
+ border: 2px solid var(--accent);
82
+ border-radius: var(--radius-md);
83
+ opacity: 0;
84
+ animation: split-btn-glow 1s ease-out;
85
+ }
86
+
87
+ @keyframes split-btn-glow {
88
+ 0% {
89
+ opacity: 1;
90
+ transform: scale(1);
91
+ }
92
+ 100% {
93
+ opacity: 0;
94
+ transform: scale(1.2);
95
+ }
96
+ }
97
+
98
+ /* Mobile: split-view cannot activate */
99
+ @media (max-width: 1023px) {
100
+ .split-pane-secondary,
101
+ .split-drag-handle {
102
+ display: none !important;
103
+ }
104
+
105
+ #splitViewBtn {
106
+ display: none !important;
107
+ }
108
+ }
109
+
110
+ /* ==========================================================================
111
+ Secondary Chat Pane
112
+ ========================================================================== */
113
+
114
+ .split-pane-secondary {
115
+ display: none;
116
+ flex-direction: column;
117
+ background: var(--bg-app, var(--bg));
118
+ overflow: hidden;
119
+ min-width: 0;
120
+ border-left: 1px solid var(--border);
121
+ }
122
+
123
+ @media (min-width: 1024px) {
124
+ body.split-view-active .split-pane-secondary {
125
+ display: flex;
126
+ flex: 0 0 calc((1 - var(--split-ratio)) * 100%);
127
+ min-width: var(--split-min-width);
128
+ animation: split-pane-slide-in var(--split-transition);
129
+ }
130
+ }
131
+
132
+ @keyframes split-pane-slide-in {
133
+ from {
134
+ opacity: 0;
135
+ transform: translateX(20px);
136
+ }
137
+ to {
138
+ opacity: 1;
139
+ transform: translateX(0);
140
+ }
141
+ }
142
+
143
+ /* ==========================================================================
144
+ Secondary Pane Header
145
+ ========================================================================== */
146
+
147
+ .split-secondary-header {
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: space-between;
151
+ padding: var(--space-3) var(--space-4);
152
+ border-bottom: 1px solid var(--border);
153
+ background: var(--bg-header, var(--bg));
154
+ flex-shrink: 0;
155
+ min-height: 48px;
156
+ }
157
+
158
+ .split-secondary-info {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: var(--space-3);
162
+ flex: 1;
163
+ min-width: 0;
164
+ }
165
+
166
+ .split-secondary-icon {
167
+ font-size: var(--text-xl);
168
+ flex-shrink: 0;
169
+ width: 28px;
170
+ height: 28px;
171
+ border-radius: 50%;
172
+ object-fit: cover;
173
+ }
174
+
175
+ .split-secondary-name {
176
+ font-size: var(--text-base);
177
+ font-weight: 600;
178
+ color: var(--text);
179
+ white-space: nowrap;
180
+ overflow: hidden;
181
+ text-overflow: ellipsis;
182
+ }
183
+
184
+ .split-context-badge {
185
+ margin-left: auto;
186
+ }
187
+
188
+ .split-secondary-status {
189
+ display: flex;
190
+ align-items: center;
191
+ }
192
+
193
+ .split-secondary-status .status-dot {
194
+ width: 8px;
195
+ height: 8px;
196
+ border-radius: 50%;
197
+ background: var(--neutral);
198
+ animation: pulse 2s ease-in-out infinite;
199
+ }
200
+
201
+ .split-secondary-status .status-dot.connected {
202
+ background: var(--success);
203
+ }
204
+
205
+ .split-secondary-actions {
206
+ display: flex;
207
+ align-items: center;
208
+ gap: var(--space-2);
209
+ }
210
+
211
+ .split-secondary-switch,
212
+ .split-secondary-close {
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ width: 32px;
217
+ height: 32px;
218
+ border: none;
219
+ background: transparent;
220
+ color: var(--text-muted);
221
+ cursor: pointer;
222
+ border-radius: var(--radius-md);
223
+ transition: all 0.2s ease;
224
+ flex-shrink: 0;
225
+ }
226
+
227
+ .split-secondary-switch:hover,
228
+ .split-secondary-close:hover {
229
+ background: var(--accent-10);
230
+ color: var(--accent);
231
+ }
232
+
233
+ .split-secondary-switch:active,
234
+ .split-secondary-close:active {
235
+ transform: scale(0.95);
236
+ }
237
+
238
+ .split-secondary-close {
239
+ font-size: var(--text-2xl);
240
+ line-height: 1;
241
+ }
242
+
243
+ /* ==========================================================================
244
+ Secondary Pane Messages Area
245
+ Match primary .messages styling from app.css
246
+ ========================================================================== */
247
+
248
+ .split-secondary-messages {
249
+ flex: 1;
250
+ overflow-y: auto;
251
+ overflow-x: hidden;
252
+ padding: var(--space-4);
253
+ padding-bottom: 80px;
254
+ display: flex;
255
+ flex-direction: column;
256
+ gap: var(--space-2);
257
+ scroll-behavior: smooth;
258
+ -webkit-overflow-scrolling: touch;
259
+ }
260
+
261
+ /* Push messages to bottom when few */
262
+ .split-secondary-messages .messages-spacer {
263
+ flex: 1;
264
+ }
265
+
266
+ /* Scrollbar — match primary */
267
+ .split-secondary-messages::-webkit-scrollbar { width: 8px; }
268
+ .split-secondary-messages::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); }
269
+ .split-secondary-messages::-webkit-scrollbar-thumb {
270
+ background: var(--accent-30);
271
+ border-radius: 4px;
272
+ }
273
+ .split-secondary-messages::-webkit-scrollbar-thumb:hover {
274
+ background: var(--accent-50);
275
+ }
276
+
277
+ /* Message bubbles — match primary .message */
278
+ .split-secondary-messages .message {
279
+ max-width: 85%;
280
+ padding: var(--space-2) var(--space-3);
281
+ border-radius: var(--radius-lg);
282
+ font-size: var(--text-base);
283
+ line-height: 1.6;
284
+ color: var(--text);
285
+ word-wrap: break-word;
286
+ overflow-wrap: break-word;
287
+ animation: message-fade-in 0.3s ease-out;
288
+ }
289
+
290
+ .split-secondary-messages .message.assistant {
291
+ align-self: flex-start;
292
+ background: var(--assistant-bubble, transparent);
293
+ }
294
+
295
+ .split-secondary-messages .message.user {
296
+ align-self: flex-end;
297
+ background: var(--user-bubble);
298
+ }
299
+
300
+ /* Empty state */
301
+ .split-secondary-messages:empty::after,
302
+ .split-secondary-messages:has(> .messages-spacer:only-child)::after {
303
+ content: 'Select an agent to start a conversation';
304
+ color: var(--text-muted);
305
+ font-size: var(--text-base);
306
+ text-align: center;
307
+ align-self: center;
308
+ pointer-events: none;
309
+ }
310
+
311
+ /* ==========================================================================
312
+ Secondary Pane Input Area
313
+ Match primary .input-area / .text-field / .send-btn from app.css
314
+ ========================================================================== */
315
+
316
+ .split-secondary-input {
317
+ padding: var(--space-3) var(--space-4);
318
+ background: transparent;
319
+ border-top: 1px solid var(--border);
320
+ flex-shrink: 0;
321
+ }
322
+
323
+ .split-input-row {
324
+ display: flex;
325
+ align-items: flex-end;
326
+ gap: var(--space-2);
327
+ }
328
+
329
+ /* Match primary .text-field — pill shape with own bg/border */
330
+ .split-text-field {
331
+ flex: 1;
332
+ background: var(--bg-input);
333
+ border: 1px solid var(--border);
334
+ border-radius: var(--radius-2xl);
335
+ padding: var(--space-2) var(--space-4);
336
+ color: var(--text);
337
+ font-size: var(--text-base);
338
+ font-family: 'Nunito Sans', -apple-system, BlinkMacSystemFont, sans-serif;
339
+ outline: none;
340
+ transition: border-color 0.15s;
341
+ resize: none;
342
+ min-height: var(--space-10);
343
+ max-height: 150px;
344
+ overflow-y: auto;
345
+ line-height: 1.4;
346
+ }
347
+
348
+ .split-text-field:focus {
349
+ border-color: var(--accent);
350
+ box-shadow: 0 0 15px var(--accent-20);
351
+ }
352
+
353
+ .split-text-field:focus-visible {
354
+ outline: 2px solid var(--accent);
355
+ outline-offset: 2px;
356
+ }
357
+
358
+ .split-text-field::placeholder {
359
+ color: var(--text-dim);
360
+ }
361
+
362
+ /* Match primary .send-btn — circle with glow */
363
+ .split-send-btn {
364
+ width: 44px;
365
+ height: 44px;
366
+ min-width: 44px;
367
+ min-height: 44px;
368
+ border-radius: var(--radius-full);
369
+ border: none;
370
+ background: var(--accent);
371
+ color: var(--text-inverse, #fff);
372
+ cursor: pointer;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ transition: all 0.15s;
377
+ box-shadow: 0 0 20px var(--accent-30);
378
+ flex-shrink: 0;
379
+ }
380
+
381
+ .split-send-btn:hover {
382
+ background: var(--accent-hover);
383
+ box-shadow: 0 0 30px var(--accent-40);
384
+ }
385
+
386
+ .split-send-btn:active {
387
+ transform: scale(0.95);
388
+ }
389
+
390
+ .split-send-btn:disabled {
391
+ opacity: 0.4;
392
+ cursor: not-allowed;
393
+ transform: none;
394
+ }
395
+
396
+ .split-send-btn svg {
397
+ width: 18px;
398
+ height: 18px;
399
+ }
400
+
401
+ /* ==========================================================================
402
+ Session Picker Overlay
403
+ ========================================================================== */
404
+
405
+ .split-chat-picker {
406
+ position: absolute;
407
+ inset: 0;
408
+ background: var(--bg);
409
+ z-index: 10;
410
+ display: flex;
411
+ flex-direction: column;
412
+ overflow: hidden;
413
+ }
414
+
415
+ .split-chat-picker-header {
416
+ display: flex;
417
+ align-items: center;
418
+ justify-content: space-between;
419
+ padding: var(--space-3) var(--space-4);
420
+ border-bottom: 1px solid var(--border);
421
+ flex-shrink: 0;
422
+ }
423
+
424
+ .split-chat-picker-title {
425
+ font-size: var(--text-base);
426
+ font-weight: 600;
427
+ color: var(--text);
428
+ text-transform: uppercase;
429
+ letter-spacing: 0.05em;
430
+ }
431
+
432
+ .split-chat-picker-close {
433
+ width: 32px;
434
+ height: 32px;
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ background: transparent;
439
+ border: none;
440
+ color: var(--text-muted);
441
+ font-size: var(--text-xl);
442
+ cursor: pointer;
443
+ border-radius: var(--radius-md);
444
+ transition: all 0.15s;
445
+ }
446
+
447
+ .split-chat-picker-close:hover {
448
+ background: var(--accent-10);
449
+ color: var(--accent);
450
+ }
451
+
452
+ .split-chat-picker-list {
453
+ flex: 1;
454
+ overflow-y: auto;
455
+ padding: var(--space-3);
456
+ display: flex;
457
+ flex-direction: column;
458
+ gap: var(--space-2);
459
+ }
460
+
461
+ .split-chat-picker-item {
462
+ display: flex;
463
+ align-items: center;
464
+ gap: var(--space-3);
465
+ padding: var(--space-3) var(--space-4);
466
+ background: var(--bg-input, var(--bg-secondary));
467
+ border: 1px solid var(--border);
468
+ border-radius: var(--radius-lg);
469
+ color: var(--text);
470
+ font-size: var(--text-base);
471
+ cursor: pointer;
472
+ transition: all 0.15s;
473
+ text-align: left;
474
+ width: 100%;
475
+ }
476
+
477
+ .split-chat-picker-item:hover {
478
+ border-color: var(--accent);
479
+ background: var(--accent-10);
480
+ }
481
+
482
+ .split-chat-picker-item.active {
483
+ border-color: var(--accent);
484
+ box-shadow: 0 0 15px var(--accent-20);
485
+ }
486
+
487
+ .split-chat-picker-item.primary {
488
+ opacity: 0.5;
489
+ }
490
+
491
+ .split-chat-picker-avatar {
492
+ width: 32px;
493
+ height: 32px;
494
+ border-radius: 50%;
495
+ object-fit: cover;
496
+ flex-shrink: 0;
497
+ }
498
+
499
+ .split-chat-picker-emoji {
500
+ font-size: var(--text-xl);
501
+ flex-shrink: 0;
502
+ }
503
+
504
+ .split-chat-picker-name {
505
+ flex: 1;
506
+ font-weight: 500;
507
+ }
508
+
509
+ .split-chat-picker-badge {
510
+ font-size: var(--text-xs);
511
+ font-weight: 600;
512
+ padding: 2px 8px;
513
+ border-radius: var(--radius-full);
514
+ background: var(--border);
515
+ color: var(--text-muted);
516
+ text-transform: uppercase;
517
+ letter-spacing: 0.05em;
518
+ }
519
+
520
+ .split-chat-picker-badge.active {
521
+ background: var(--accent-20);
522
+ color: var(--accent);
523
+ }
524
+
525
+ .split-chat-picker-empty {
526
+ text-align: center;
527
+ color: var(--text-muted);
528
+ padding: var(--space-6);
529
+ font-size: var(--text-base);
530
+ }
531
+
532
+ /* ==========================================================================
533
+ Drag Handle
534
+ ========================================================================== */
535
+
536
+ .split-drag-handle {
537
+ display: none;
538
+ position: relative;
539
+ width: var(--split-handle-width);
540
+ background: var(--border);
541
+ cursor: col-resize;
542
+ flex-shrink: 0;
543
+ transition: background 0.2s ease;
544
+ user-select: none;
545
+ }
546
+
547
+ @media (min-width: 1024px) {
548
+ body.split-view-active .split-drag-handle {
549
+ display: block;
550
+ }
551
+
552
+ .split-drag-handle:hover {
553
+ background: var(--accent-30);
554
+ }
555
+
556
+ .split-drag-handle:active,
557
+ .split-drag-handle.dragging {
558
+ background: var(--accent);
559
+ }
560
+
561
+ /* Visual indicator on hover */
562
+ .split-drag-handle::before {
563
+ content: '';
564
+ position: absolute;
565
+ top: 50%;
566
+ left: 50%;
567
+ transform: translate(-50%, -50%);
568
+ width: 20px;
569
+ height: 40px;
570
+ background: var(--accent-20);
571
+ border-radius: var(--radius-full);
572
+ opacity: 0;
573
+ transition: opacity 0.2s ease;
574
+ pointer-events: none;
575
+ }
576
+
577
+ .split-drag-handle:hover::before {
578
+ opacity: 1;
579
+ }
580
+ }
581
+
582
+ /* ==========================================================================
583
+ Accessibility
584
+ ========================================================================== */
585
+
586
+ .split-drag-handle:focus-visible {
587
+ outline: 2px solid var(--accent);
588
+ outline-offset: 2px;
589
+ }
590
+
591
+ /* Reduce motion */
592
+ @media (prefers-reduced-motion: reduce) {
593
+ .split-pane-secondary,
594
+ .split-drag-handle,
595
+ #splitViewBtn::after,
596
+ .split-input-row {
597
+ animation: none !important;
598
+ transition: none !important;
599
+ }
600
+ }
601
+
602
+ /* ==========================================================================
603
+ Loading State
604
+ ========================================================================== */
605
+
606
+ .split-secondary-messages.loading::after {
607
+ content: '';
608
+ display: block;
609
+ width: 40px;
610
+ height: 40px;
611
+ margin: var(--space-4) auto;
612
+ border: 3px solid var(--border);
613
+ border-top-color: var(--accent);
614
+ border-radius: 50%;
615
+ animation: spin 1s linear infinite;
616
+ }
617
+
618
+ @keyframes spin {
619
+ to { transform: rotate(360deg); }
620
+ }
621
+
622
+ /* ==========================================================================
623
+ Override: Primary chat max-width in split mode
624
+ When split is active, primary should use all its allocated space
625
+ ========================================================================== */
626
+
627
+ @media (min-width: 1024px) {
628
+ body.split-view-active .app {
629
+ max-width: none;
630
+ }
631
+
632
+ body.split-view-active .split-pane-primary .messages {
633
+ max-width: none;
634
+ }
635
+ }
636
+
637
+ /* ==========================================================================
638
+ Side Panel Integration in Split-View Mode
639
+ When split-view AND a panel are both open, the side panel should
640
+ flow alongside the chat container instead of overlaying.
641
+ ========================================================================== */
642
+
643
+ @media (min-width: 1024px) {
644
+ /* Override: make side panel flow in layout-wrapper instead of fixed overlay */
645
+ body.split-view-active.panel-open .side-panel {
646
+ position: relative;
647
+ top: auto;
648
+ right: auto;
649
+ width: 25%;
650
+ min-width: 280px;
651
+ max-width: 400px;
652
+ height: 100%;
653
+ z-index: auto;
654
+ flex-shrink: 0;
655
+ }
656
+
657
+ /* App shrinks to make room for the panel in the flow */
658
+ body.split-view-active.panel-open .app {
659
+ max-width: none;
660
+ margin-right: 0;
661
+ flex: 1;
662
+ min-width: 0;
663
+ }
664
+ }
665
+
666
+ /* Streaming animation for split view messages
667
+ Uses @keyframes streamingRainbow defined in app.css.
668
+ Add border so border-color animation is visible. */
669
+ .split-secondary-messages .message.streaming {
670
+ position: relative;
671
+ border: 1px solid transparent;
672
+ animation: streamingRainbow 4s linear infinite;
673
+ }