@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,211 @@
1
+ /**
2
+ * Split View Drag Resize Handler
3
+ * Manages the drag handle for resizing split-view panes
4
+ * Targets .split-chat-container (the flex container inside .app)
5
+ */
6
+
7
+ import { UplinkCore } from './core.js';
8
+
9
+ const STORAGE_KEY = 'uplink-split-ratio';
10
+ const MIN_RATIO = 0.3;
11
+ const MAX_RATIO = 0.7;
12
+
13
+ let isDragging = false;
14
+ let container = null;
15
+
16
+ /**
17
+ * Initialize drag resize functionality
18
+ */
19
+ function init() {
20
+ const handle = document.getElementById('splitDragHandle');
21
+ container = document.getElementById('splitChatContainer');
22
+
23
+ if (!handle || !container) {
24
+ console.warn('[split-resize] Required elements not found');
25
+ return;
26
+ }
27
+
28
+ // Restore saved ratio
29
+ restoreRatio();
30
+
31
+ // Mouse events
32
+ handle.addEventListener('mousedown', handleMouseDown);
33
+ document.addEventListener('mousemove', handleMouseMove);
34
+ document.addEventListener('mouseup', handleMouseUp);
35
+
36
+ // Touch events for tablets
37
+ handle.addEventListener('touchstart', handleTouchStart, { passive: false });
38
+ document.addEventListener('touchmove', handleTouchMove, { passive: false });
39
+ document.addEventListener('touchend', handleTouchEnd);
40
+
41
+ // Keyboard accessibility (arrow keys to resize)
42
+ handle.addEventListener('keydown', handleKeyDown);
43
+ handle.setAttribute('tabindex', '0');
44
+ handle.setAttribute('role', 'separator');
45
+ }
46
+
47
+ /**
48
+ * Restore split ratio from localStorage
49
+ */
50
+ function restoreRatio() {
51
+ try {
52
+ const savedRatio = localStorage.getItem(STORAGE_KEY);
53
+ if (savedRatio !== null) {
54
+ const ratio = parseFloat(savedRatio);
55
+ if (!isNaN(ratio) && ratio >= MIN_RATIO && ratio <= MAX_RATIO) {
56
+ setSplitRatio(ratio);
57
+ }
58
+ }
59
+ } catch (err) {
60
+ console.warn('[split-resize] Failed to restore ratio:', err);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Save split ratio to localStorage
66
+ */
67
+ function saveRatio(ratio) {
68
+ try {
69
+ localStorage.setItem(STORAGE_KEY, ratio.toString());
70
+ } catch (err) {
71
+ console.warn('[split-resize] Failed to save ratio:', err);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Set the split ratio CSS custom property on the container
77
+ */
78
+ function setSplitRatio(ratio) {
79
+ const clamped = Math.max(MIN_RATIO, Math.min(MAX_RATIO, ratio));
80
+ if (container) {
81
+ container.style.setProperty('--split-ratio', clamped.toString());
82
+ }
83
+ // Also set on :root for any global references
84
+ document.documentElement.style.setProperty('--split-ratio', clamped.toString());
85
+ }
86
+
87
+ /**
88
+ * Calculate ratio from mouse/touch X position relative to the container
89
+ */
90
+ function calculateRatioFromX(clientX) {
91
+ if (!container) return 0.5;
92
+ const rect = container.getBoundingClientRect();
93
+ const x = clientX - rect.left;
94
+ return x / rect.width;
95
+ }
96
+
97
+ /**
98
+ * Mouse down handler
99
+ */
100
+ function handleMouseDown(e) {
101
+ e.preventDefault();
102
+ isDragging = true;
103
+
104
+ e.currentTarget.classList.add('dragging');
105
+ document.body.classList.add('split-dragging');
106
+ document.body.style.cursor = 'col-resize';
107
+ document.body.style.userSelect = 'none';
108
+ }
109
+
110
+ /**
111
+ * Mouse move handler
112
+ */
113
+ function handleMouseMove(e) {
114
+ if (!isDragging) return;
115
+ const ratio = calculateRatioFromX(e.clientX);
116
+ setSplitRatio(ratio);
117
+ }
118
+
119
+ /**
120
+ * Mouse up handler
121
+ */
122
+ function handleMouseUp() {
123
+ if (!isDragging) return;
124
+ isDragging = false;
125
+
126
+ const handle = document.getElementById('splitDragHandle');
127
+ if (handle) handle.classList.remove('dragging');
128
+ document.body.classList.remove('split-dragging');
129
+
130
+ document.body.style.cursor = '';
131
+ document.body.style.userSelect = '';
132
+
133
+ // Save final ratio
134
+ const finalRatio = parseFloat(
135
+ getComputedStyle(container || document.documentElement).getPropertyValue('--split-ratio') || '0.5'
136
+ );
137
+ saveRatio(finalRatio);
138
+ }
139
+
140
+ /**
141
+ * Touch start handler
142
+ */
143
+ function handleTouchStart(e) {
144
+ if (e.touches.length !== 1) return;
145
+ e.preventDefault();
146
+ isDragging = true;
147
+ e.currentTarget.classList.add('dragging');
148
+ document.body.classList.add('split-dragging');
149
+ }
150
+
151
+ /**
152
+ * Touch move handler
153
+ */
154
+ function handleTouchMove(e) {
155
+ if (!isDragging || e.touches.length !== 1) return;
156
+ e.preventDefault();
157
+ const ratio = calculateRatioFromX(e.touches[0].clientX);
158
+ setSplitRatio(ratio);
159
+ }
160
+
161
+ /**
162
+ * Touch end handler
163
+ */
164
+ function handleTouchEnd() {
165
+ if (!isDragging) return;
166
+ isDragging = false;
167
+
168
+ const handle = document.getElementById('splitDragHandle');
169
+ if (handle) handle.classList.remove('dragging');
170
+ document.body.classList.remove('split-dragging');
171
+
172
+ const finalRatio = parseFloat(
173
+ getComputedStyle(container || document.documentElement).getPropertyValue('--split-ratio') || '0.5'
174
+ );
175
+ saveRatio(finalRatio);
176
+ }
177
+
178
+ /**
179
+ * Keyboard handler (arrow keys to resize)
180
+ */
181
+ function handleKeyDown(e) {
182
+ const currentRatio = parseFloat(
183
+ getComputedStyle(container || document.documentElement).getPropertyValue('--split-ratio') || '0.5'
184
+ );
185
+ let newRatio = currentRatio;
186
+ let handled = false;
187
+
188
+ switch(e.key) {
189
+ case 'ArrowLeft': newRatio -= 0.05; handled = true; break;
190
+ case 'ArrowRight': newRatio += 0.05; handled = true; break;
191
+ case 'Home': newRatio = MIN_RATIO; handled = true; break;
192
+ case 'End': newRatio = MAX_RATIO; handled = true; break;
193
+ case 'Enter':
194
+ case ' ': newRatio = 0.5; handled = true; break;
195
+ }
196
+
197
+ if (handled) {
198
+ e.preventDefault();
199
+ setSplitRatio(newRatio);
200
+ saveRatio(newRatio);
201
+ }
202
+ }
203
+
204
+ // Register with core for coordinated initialization
205
+ UplinkCore.registerModule('split-resize', init);
206
+
207
+ // Export for debugging
208
+ export const SplitResize = { setSplitRatio, saveRatio, restoreRatio };
209
+
210
+ // Backward compat: assign to window
211
+ window.SplitResize = SplitResize;
@@ -0,0 +1,340 @@
1
+ // ============================================
2
+ // SPLIT VIEW - Desktop panel management
3
+ // ============================================
4
+
5
+ import { UplinkCore } from './core.js';
6
+
7
+ const DESKTOP_BREAKPOINT = 1024;
8
+ const MAX_INIT_RETRIES = 5;
9
+ const INIT_RETRY_DELAY_MS = 100;
10
+ let initRetryCount = 0;
11
+
12
+ let currentPanel = null;
13
+ let layoutWrapper = null;
14
+ let sidePanel = null;
15
+ let sidePanelTitle = null;
16
+ let sidePanelContent = null;
17
+ let sidePanelClose = null;
18
+
19
+ // Panel configurations - titles match Activity panel style (uppercase, no emoji)
20
+ const panels = {
21
+ satellites: {
22
+ title: 'SATELLITES',
23
+ getContent: () => document.querySelector('.satellite-navigator')
24
+ },
25
+ activity: {
26
+ title: 'ACTIVITY',
27
+ getContent: () => document.querySelector('.dev-panel')
28
+ },
29
+ settings: {
30
+ title: 'SETTINGS',
31
+ getContent: () => document.querySelector('.settings-panel')
32
+ }
33
+ };
34
+
35
+ function isDesktop() {
36
+ return window.innerWidth >= DESKTOP_BREAKPOINT;
37
+ }
38
+
39
+ function init() {
40
+ layoutWrapper = document.querySelector('.layout-wrapper');
41
+ sidePanel = document.getElementById('sidePanel');
42
+ sidePanelTitle = document.getElementById('sidePanelTitle');
43
+ sidePanelContent = document.getElementById('sidePanelContent');
44
+ sidePanelClose = document.getElementById('sidePanelClose');
45
+
46
+ if (!layoutWrapper || !sidePanel) {
47
+ if (initRetryCount < MAX_INIT_RETRIES) {
48
+ initRetryCount++;
49
+ logger.warn(`Split view: Required elements not found, retrying (${initRetryCount}/${MAX_INIT_RETRIES})...`);
50
+ setTimeout(init, INIT_RETRY_DELAY_MS * initRetryCount);
51
+ return;
52
+ }
53
+ logger.warn('Split view: Required elements not found after max retries');
54
+ return;
55
+ }
56
+
57
+ // Close button handler
58
+ sidePanelClose?.addEventListener('click', closePanel);
59
+
60
+ // Escape key to close
61
+ document.addEventListener('keydown', (e) => {
62
+ if (e.key === 'Escape' && currentPanel) {
63
+ closePanel();
64
+ }
65
+ });
66
+
67
+ // Handle resize - close panel if going to mobile
68
+ // Debounced to avoid excessive layout checks during resize
69
+ window.addEventListener('resize', UplinkCore.debounce(() => {
70
+ if (!isDesktop() && currentPanel) {
71
+ closePanel();
72
+ }
73
+ }, 150));
74
+
75
+ // Intercept panel button clicks
76
+ setupPanelInterceptors();
77
+ }
78
+
79
+ function setupPanelInterceptors() {
80
+ // Satellites button
81
+ const satellitesBtn = document.getElementById('satellitesBtn');
82
+ if (satellitesBtn) {
83
+ satellitesBtn.addEventListener('click', (e) => {
84
+ if (isDesktop()) {
85
+ e.stopPropagation();
86
+ e.stopImmediatePropagation();
87
+ togglePanel('satellites');
88
+ }
89
+ }, true); // Capture phase to intercept before other handlers
90
+ }
91
+
92
+ // Activity button
93
+ const activityBtn = document.getElementById('activityBtn');
94
+ if (activityBtn) {
95
+ activityBtn.addEventListener('click', (e) => {
96
+ if (isDesktop()) {
97
+ e.stopPropagation();
98
+ e.stopImmediatePropagation();
99
+ togglePanel('activity');
100
+ }
101
+ }, true);
102
+ }
103
+
104
+ // Settings button
105
+ const settingsBtn = document.getElementById('settingsBtn');
106
+ if (settingsBtn) {
107
+ settingsBtn.addEventListener('click', (e) => {
108
+ if (isDesktop()) {
109
+ e.stopPropagation();
110
+ e.stopImmediatePropagation();
111
+ togglePanel('settings');
112
+ }
113
+ }, true);
114
+ }
115
+ }
116
+
117
+ function togglePanel(panelName) {
118
+ console.log('[SplitView] togglePanel called, panelName:', panelName, 'currentPanel:', currentPanel);
119
+ if (currentPanel === panelName) {
120
+ closePanel();
121
+ } else {
122
+ openPanel(panelName);
123
+ }
124
+ }
125
+
126
+ function openPanel(panelName) {
127
+ const config = panels[panelName];
128
+ if (!config) return;
129
+
130
+ // Close any existing panel first
131
+ if (currentPanel && currentPanel !== panelName) {
132
+ restorePanelContent(currentPanel);
133
+ }
134
+
135
+ currentPanel = panelName;
136
+
137
+ // Update title
138
+ sidePanelTitle.textContent = config.title;
139
+
140
+ // Get the panel content element
141
+ // For dynamically created panels, we may need to wait or trigger their creation
142
+ let contentEl = config.getContent();
143
+
144
+ if (contentEl) {
145
+ // Move content into side panel (CSS handles the styling override)
146
+ sidePanelContent.innerHTML = '';
147
+ sidePanelContent.appendChild(contentEl);
148
+ // Ensure it's visible
149
+ contentEl.classList.add('visible');
150
+ } else {
151
+ // Panel not created yet - trigger its creation
152
+ triggerPanelCreation(panelName);
153
+ // Try again after a short delay
154
+ setTimeout(() => {
155
+ contentEl = config.getContent();
156
+ if (contentEl) {
157
+ sidePanelContent.innerHTML = '';
158
+ sidePanelContent.appendChild(contentEl);
159
+ contentEl.classList.add('visible');
160
+ }
161
+ }, 50);
162
+ }
163
+
164
+ // Add class to activate split layout (body for CSS 70/30 split)
165
+ document.body.classList.add('panel-open');
166
+ layoutWrapper.classList.add('panel-open');
167
+ sidePanel.classList.add('visible');
168
+
169
+ // Update button states
170
+ updateButtonStates(panelName);
171
+ }
172
+
173
+ function closePanel() {
174
+ console.log('[SplitView] closePanel called, currentPanel:', currentPanel);
175
+ console.trace('[SplitView] closePanel stack trace');
176
+ if (!currentPanel) return;
177
+
178
+ restorePanelContent(currentPanel);
179
+
180
+ document.body.classList.remove('panel-open');
181
+ layoutWrapper.classList.remove('panel-open');
182
+ sidePanel.classList.remove('visible');
183
+
184
+ updateButtonStates(null);
185
+ currentPanel = null;
186
+ }
187
+
188
+ function restorePanelContent(panelName) {
189
+ const config = panels[panelName];
190
+ if (!config) return;
191
+
192
+ const contentEl = config.getContent();
193
+ if (contentEl && sidePanelContent.contains(contentEl)) {
194
+ // Move back to body and hide
195
+ document.body.appendChild(contentEl);
196
+ contentEl.classList.remove('visible');
197
+ }
198
+ }
199
+
200
+ function triggerPanelCreation(panelName) {
201
+ // Dispatch a custom event that the panel modules can listen to
202
+ // or directly call their init functions if available
203
+ if (panelName === 'satellites' && window.UplinkSatellites) {
204
+ window.UplinkSatellites.toggleNavigator();
205
+ } else if (panelName === 'activity' && window.UplinkDeveloper) {
206
+ window.UplinkDeveloper.show();
207
+ } else if (panelName === 'settings') {
208
+ // Settings panel is static in HTML, just show it
209
+ const settingsPanel = document.getElementById('settingsPanel');
210
+ if (settingsPanel) {
211
+ settingsPanel.classList.add('visible');
212
+ }
213
+ }
214
+ }
215
+
216
+ function updateButtonStates(activePanel) {
217
+ const buttons = {
218
+ satellites: document.getElementById('satellitesBtn'),
219
+ activity: document.getElementById('activityBtn'),
220
+ settings: document.getElementById('settingsBtn')
221
+ };
222
+
223
+ Object.entries(buttons).forEach(([name, btn]) => {
224
+ if (btn) {
225
+ btn.classList.toggle('active', name === activePanel);
226
+ }
227
+ });
228
+ }
229
+
230
+ // ============================================
231
+ // DUAL CHAT (SPLIT VIEW)
232
+ // ============================================
233
+
234
+ let splitActive = false;
235
+
236
+ function setupSplitViewButton() {
237
+ const splitBtn = document.getElementById('splitViewBtn');
238
+ if (!splitBtn) return;
239
+
240
+ splitBtn.addEventListener('click', () => {
241
+ if (!isDesktop()) return;
242
+ toggleSplitView();
243
+ });
244
+
245
+ // Close button in secondary pane
246
+ const closeBtn = document.getElementById('splitSecondaryClose');
247
+ if (closeBtn) {
248
+ closeBtn.addEventListener('click', () => closeSplitView());
249
+ }
250
+
251
+ // Switch session button listener is handled by split-chat.js
252
+ // (removed duplicate listener to prevent double-firing)
253
+ }
254
+
255
+ function toggleSplitView() {
256
+ if (splitActive) {
257
+ closeSplitView();
258
+ } else {
259
+ openSplitView();
260
+ }
261
+ }
262
+
263
+ function openSplitView() {
264
+ if (!isDesktop()) return;
265
+
266
+ splitActive = true;
267
+ document.body.classList.add('split-view-active');
268
+
269
+ // Show the secondary pane and drag handle
270
+ const secondary = document.getElementById('splitSecondary');
271
+ const dragHandle = document.getElementById('splitDragHandle');
272
+ if (secondary) secondary.style.display = 'flex';
273
+ if (dragHandle) dragHandle.style.display = 'block';
274
+
275
+ // Update button state
276
+ const splitBtn = document.getElementById('splitViewBtn');
277
+ if (splitBtn) splitBtn.classList.add('active');
278
+
279
+ // Show the session picker if no session is active
280
+ if (window.UplinkSplitChat && !window.UplinkSplitChat.isActive()) {
281
+ window.UplinkSplitChat.showPicker();
282
+ }
283
+ }
284
+
285
+ function closeSplitView() {
286
+ splitActive = false;
287
+ document.body.classList.remove('split-view-active');
288
+
289
+ // Hide the secondary pane and drag handle
290
+ const secondary = document.getElementById('splitSecondary');
291
+ const dragHandle = document.getElementById('splitDragHandle');
292
+ if (secondary) secondary.style.display = 'none';
293
+ if (dragHandle) dragHandle.style.display = 'none';
294
+
295
+ // Update button state
296
+ const splitBtn = document.getElementById('splitViewBtn');
297
+ if (splitBtn) splitBtn.classList.remove('active');
298
+
299
+ // Close the secondary session
300
+ if (window.UplinkSplitChat) {
301
+ window.UplinkSplitChat.closeSession();
302
+ }
303
+ }
304
+
305
+ function isSplitActive() {
306
+ return splitActive;
307
+ }
308
+
309
+ // Combined init: core init + split view button setup
310
+ function initWithSplitView() {
311
+ init();
312
+ setupSplitViewButton();
313
+
314
+ window.addEventListener('resize', UplinkCore.debounce(() => {
315
+ if (!isDesktop() && splitActive) {
316
+ closeSplitView();
317
+ }
318
+ }, 150));
319
+ }
320
+
321
+ // Expose for debugging
322
+ export const UplinkSplitView = {
323
+ openPanel,
324
+ closePanel,
325
+ togglePanel,
326
+ isDesktop,
327
+ getCurrentPanel: () => currentPanel,
328
+ // Dual chat
329
+ openSplitView,
330
+ closeSplitView,
331
+ toggleSplitView,
332
+ isSplitActive
333
+ };
334
+
335
+ // Backward compat: assign to window
336
+ window.UplinkSplitView = UplinkSplitView;
337
+
338
+ // Register with core for coordinated initialization
339
+ UplinkCore.registerModule('splitview', initWithSplitView);
340
+