@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,256 @@
1
+ // ============================================
2
+ // SATELLITE SYNC MODULE
3
+ // Gateway communication and session management
4
+ // ============================================
5
+ //
6
+ // This module handles all server/gateway communication for satellites:
7
+ // - Fetching remote sessions from the gateway
8
+ // - Syncing satellite lists across devices
9
+ // - Fetching and merging chat history from gateway
10
+ // - Session retirement (deletion)
11
+ // - Session status checks
12
+ //
13
+ // Exposed as window.UplinkSatelliteSync
14
+ // ============================================
15
+
16
+ // ============================================
17
+ // DEPENDENCY HELPERS
18
+ // ============================================
19
+
20
+ function getLogger() {
21
+ return window.logger || console;
22
+ }
23
+
24
+ // ============================================
25
+ // SYNC FUNCTIONS
26
+ // ============================================
27
+
28
+ /**
29
+ * Fetch sessions from the gateway (via server) and merge any Uplink
30
+ * satellites that don't already exist in localStorage.
31
+ * This is the cross-device sync mechanism — satellites created on
32
+ * another Uplink instance show up here after their first message.
33
+ *
34
+ * @param {Object} satellites - Current satellites object (will be mutated)
35
+ * @param {string} PRIMARY_SATELLITE - ID of the primary satellite
36
+ * @param {string} currentSatellite - Currently active satellite ID
37
+ * @returns {Promise<{added: number, removed: number, satellites: Object, currentSatellite: string}>}
38
+ */
39
+ async function syncRemoteSessions(satellites, PRIMARY_SATELLITE, currentSatellite) {
40
+ try {
41
+ const res = await fetch('/api/satellite/sessions');
42
+ if (!res.ok) {
43
+ return { added: 0, removed: 0, satellites, currentSatellite };
44
+ }
45
+
46
+ const data = await res.json();
47
+ if (!data.ok || !Array.isArray(data.sessions)) {
48
+ return { added: 0, removed: 0, satellites, currentSatellite };
49
+ }
50
+
51
+ let added = 0;
52
+ let removed = 0;
53
+
54
+ // Build set of remote satellite IDs for pruning
55
+ const remoteIds = new Set(data.sessions.map(s => s.satelliteId));
56
+
57
+ // Add new remote sessions
58
+ for (const session of data.sessions) {
59
+ const { satelliteId, agentId, updatedAt } = session;
60
+
61
+ // Skip if we already have this satellite locally
62
+ if (satellites[satelliteId]) continue;
63
+
64
+ // Skip test/manual satellites
65
+ if (satelliteId.startsWith('test-')) continue;
66
+
67
+ // Derive a display name from the agentId
68
+ const agentName = deriveAgentName(agentId);
69
+
70
+ satellites[satelliteId] = {
71
+ id: satelliteId,
72
+ name: agentName,
73
+ agentId: agentId || 'main',
74
+ createdAt: updatedAt || Date.now(),
75
+ synced: true, // Mark as remotely synced (not created locally)
76
+ messages: []
77
+ };
78
+ added++;
79
+ }
80
+
81
+ // Prune: remove synced satellites that no longer exist on the gateway
82
+ // (deleted from another device). Only prune satellites we synced, not
83
+ // locally-created ones or the primary.
84
+ for (const [satId, sat] of Object.entries(satellites)) {
85
+ if (satId === PRIMARY_SATELLITE) continue;
86
+ if (!sat.synced) continue;
87
+ if (!remoteIds.has(satId)) {
88
+ // Don't delete if it's the current satellite — switch away first
89
+ if (satId === currentSatellite) {
90
+ currentSatellite = PRIMARY_SATELLITE;
91
+ }
92
+ delete satellites[satId];
93
+ removed++;
94
+ }
95
+ }
96
+
97
+ if (added > 0 || removed > 0) {
98
+ getLogger().debug('SatelliteSync: Synced — added', added, ', removed', removed, 'session(s)');
99
+ }
100
+
101
+ return { added, removed, satellites, currentSatellite };
102
+ } catch (e) {
103
+ // Non-fatal — local satellites still work
104
+ getLogger().debug('SatelliteSync: Remote sync failed (non-fatal)', e.message);
105
+ return { added: 0, removed: 0, satellites, currentSatellite };
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Derive a human-readable name from an agentId.
111
+ * E.g. "top-flight-security" → "Top Flight Security"
112
+ */
113
+ function deriveAgentName(agentId) {
114
+ if (!agentId || agentId === 'main') return 'Primary';
115
+
116
+ // Check if we have agent metadata available
117
+ const agents = window.UplinkAgents?.getAgents?.() || [];
118
+ const agent = agents.find(a => a.id === agentId);
119
+ if (agent?.name) return agent.name;
120
+ if (agent?.identity?.name) return agent.identity.name;
121
+
122
+ // Fallback: humanize the ID
123
+ return agentId
124
+ .replace(/-/g, ' ')
125
+ .replace(/\b\w/g, c => c.toUpperCase());
126
+ }
127
+
128
+ /**
129
+ * Fetch history from Gateway and merge with local satellite messages
130
+ *
131
+ * @param {string} satelliteId - ID of the satellite to fetch history for
132
+ * @param {Object} satellite - Satellite object (will be mutated with merged messages)
133
+ * @returns {Promise<void>}
134
+ */
135
+ async function fetchAndMergeGatewayHistory(satelliteId, satellite) {
136
+ try {
137
+ getLogger().debug('SatelliteSync: Fetching Gateway history for', satelliteId);
138
+ const currentAgentId = satellite?.agentId || 'main';
139
+ const response = await fetch(`/api/gateway/history?satelliteId=${satelliteId}&agentId=${currentAgentId}&limit=100`);
140
+
141
+ if (!response.ok) {
142
+ getLogger().warn('SatelliteSync: Gateway history fetch failed:', response.status);
143
+ return;
144
+ }
145
+
146
+ const data = await response.json();
147
+ if (!data.ok || !data.messages || data.messages.length === 0) {
148
+ getLogger().debug('SatelliteSync: No Gateway messages to merge');
149
+ return;
150
+ }
151
+
152
+ getLogger().debug('SatelliteSync: Got', data.messages.length, 'messages from Gateway');
153
+
154
+ if (!satellite) return;
155
+
156
+ // Gateway is source of truth for text messages.
157
+ // Preserve any local image messages since the Gateway transcript
158
+ // doesn't include image attachments.
159
+ const localImages = (satellite.messages || []).filter(m => m.imageUrl);
160
+ satellite.messages = mergeMessagesByTimestamp(localImages, data.messages);
161
+
162
+ getLogger().debug('SatelliteSync: Loaded', data.messages.length, 'messages from Gateway, preserved', localImages.length, 'local image messages');
163
+
164
+ } catch (err) {
165
+ getLogger().error('SatelliteSync: Gateway history error:', err.message);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Merge local and Gateway messages by timestamp, avoiding duplicates
171
+ */
172
+ function mergeMessagesByTimestamp(localMessages, gatewayMessages) {
173
+ // Create a map of existing local messages by timestamp+type
174
+ const localSet = new Set(
175
+ localMessages.map(m => `${m.timestamp || 0}-${m.type}-${(m.text || '').substring(0, 50)}`)
176
+ );
177
+
178
+ // Add Gateway messages that don't exist locally
179
+ const newMessages = gatewayMessages.filter(gm => {
180
+ const key = `${gm.timestamp || 0}-${gm.type}-${(gm.text || '').substring(0, 50)}`;
181
+ return !localSet.has(key);
182
+ });
183
+
184
+ // Combine and sort by timestamp
185
+ const combined = [...localMessages, ...newMessages];
186
+ combined.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
187
+
188
+ return combined;
189
+ }
190
+
191
+ /**
192
+ * Call the backend to retire a satellite's OpenClaw session
193
+ * This deletes both the session entry and transcript from OpenClaw
194
+ *
195
+ * @param {string} satId - Satellite ID to retire
196
+ * @param {string} agentId - Agent ID for the satellite
197
+ * @returns {Promise<Object|null>} Result object or null on error
198
+ */
199
+ async function retireSatelliteSession(satId, agentId) {
200
+ try {
201
+ const response = await fetch('/api/satellite/retire', {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/json' },
204
+ body: JSON.stringify({ satelliteId: satId, agentId: agentId || 'main' })
205
+ });
206
+
207
+ if (!response.ok) {
208
+ getLogger().warn('SatelliteSync: Failed to retire session', response.status);
209
+ return null;
210
+ }
211
+
212
+ const result = await response.json();
213
+ getLogger().debug('SatelliteSync: Session retired', result);
214
+
215
+ return result;
216
+ } catch (err) {
217
+ getLogger().error('SatelliteSync: Error retiring session', err);
218
+ return null;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Fetch session status from OpenClaw
224
+ *
225
+ * @param {string} satId - Satellite ID
226
+ * @param {string} satAgentId - Agent ID for the satellite
227
+ * @returns {Promise<Object|null>} Status object with {gatewayConnected, sessionKey} or null on error
228
+ */
229
+ async function fetchSessionStatus(satId, satAgentId) {
230
+ try {
231
+ const response = await fetch(`/api/session/status?satelliteId=${encodeURIComponent(satId)}&agentId=${encodeURIComponent(satAgentId)}`);
232
+ if (!response.ok) {
233
+ throw new Error(`Status check failed: ${response.status}`);
234
+ }
235
+ const data = await response.json();
236
+ return data;
237
+ } catch (err) {
238
+ getLogger().error('SatelliteSync: Session status check failed', err);
239
+ return null;
240
+ }
241
+ }
242
+
243
+ // ============================================
244
+ // PUBLIC API
245
+ // ============================================
246
+
247
+ export const UplinkSatelliteSync = {
248
+ syncRemoteSessions,
249
+ fetchAndMergeGatewayHistory,
250
+ retireSatelliteSession,
251
+ fetchSessionStatus
252
+ };
253
+
254
+ // Backward compat: assign to window
255
+ window.UplinkSatelliteSync = UplinkSatelliteSync;
256
+
@@ -0,0 +1,175 @@
1
+ // ============================================
2
+ // SATELLITE UI MODULE
3
+ // List rendering, DOM manipulation, panel UI
4
+ // ============================================
5
+
6
+ // This module contains UI rendering and event handling for satellites panel
7
+
8
+ /**
9
+ * Show notification toast
10
+ */
11
+ export function showNotification(message, type = 'success') {
12
+ const notif = window.UplinkNotifications;
13
+ if (notif && notif.show) {
14
+ notif.show(message, type);
15
+ return;
16
+ }
17
+
18
+ // Fallback toast
19
+ const toast = document.createElement('div');
20
+ toast.className = `satellite-toast satellite-toast-${type}`;
21
+ toast.textContent = message;
22
+ toast.style.cssText = `
23
+ position: fixed;
24
+ top: 20px;
25
+ right: 20px;
26
+ padding: 12px 20px;
27
+ background: ${type === 'error' ? '#ef4444' : type === 'warning' ? '#f59e0b' : '#10b981'};
28
+ color: white;
29
+ border-radius: 8px;
30
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
31
+ z-index: 100000;
32
+ animation: slideIn 0.3s ease-out;
33
+ `;
34
+ document.body.appendChild(toast);
35
+ setTimeout(() => {
36
+ toast.style.opacity = '0';
37
+ toast.style.transition = 'opacity 0.3s';
38
+ setTimeout(() => toast.remove(), 300);
39
+ }, 3000);
40
+ }
41
+
42
+ /**
43
+ * Build satellite list HTML
44
+ */
45
+ export function buildSatelliteList(satellites, currentSatellite, defaultSatellite) {
46
+ const sortedIds = Object.keys(satellites).sort((a, b) => {
47
+ if (a === defaultSatellite) return -1;
48
+ if (b === defaultSatellite) return 1;
49
+ const aTime = satellites[a].createdAt || 0;
50
+ const bTime = satellites[b].createdAt || 0;
51
+ return bTime - aTime;
52
+ });
53
+
54
+ return sortedIds.map(id => {
55
+ const sat = satellites[id];
56
+ const isActive = id === currentSatellite;
57
+ const isDefault = id === defaultSatellite;
58
+ const msgCount = (sat.messages || []).length;
59
+ const lastMsg = sat.messages?.[sat.messages.length - 1];
60
+ const preview = lastMsg ? (lastMsg.text?.slice(0, 50) || '').replace(/\n/g, ' ') : 'No messages';
61
+ const timestamp = sat.createdAt ? new Date(sat.createdAt).toLocaleDateString() : '';
62
+ const isPending = sat._pendingDelete || false;
63
+
64
+ let badges = '';
65
+ if (isDefault && id !== 'main') badges += '<span class="satellite-badge satellite-badge-default">★</span>';
66
+ if (sat.embedded) badges += '<span class="satellite-badge satellite-badge-embedded">🔗</span>';
67
+
68
+ const agentBadge = sat.agentId && sat.agentId !== 'main'
69
+ ? `<span class="satellite-agent-badge" title="Agent: ${sat.agentId}">${sat.agentId}</span>`
70
+ : '';
71
+
72
+ const agentId = sat.agentId || 'main';
73
+ const avatarSrc = `/img/agents/${agentId}.png`;
74
+
75
+ return `
76
+ <div class="satellite-item ${isActive ? 'active' : ''} ${isPending ? 'pending-delete' : ''}"
77
+ data-satellite-id="${id}" role="option" aria-selected="${isActive}">
78
+ <img class="satellite-item-avatar" src="${avatarSrc}" alt="${sat.name || id}" onerror="this.src='/img/agents/default.png'">
79
+ <div class="satellite-item-main">
80
+ <span class="satellite-item-name">${sat.name || id}</span>
81
+ ${badges}
82
+ ${agentBadge}
83
+ <span class="satellite-item-count">${msgCount}</span>
84
+ </div>
85
+ <div class="satellite-item-preview">${preview}</div>
86
+ ${timestamp ? `<div class="satellite-item-timestamp">${timestamp}</div>` : ''}
87
+ ${!isDefault || id === 'main' ? `
88
+ <div class="satellite-item-actions">
89
+ ${id !== 'main' && !isDefault ? '<button class="satellite-action-btn satellite-action-primary" data-action="setPrimary" title="Set as default">★</button>' : ''}
90
+ <button class="satellite-action-btn satellite-action-rename" data-action="rename" title="Rename">✏</button>
91
+ ${id !== 'main' ? '<button class="satellite-action-btn satellite-action-delete" data-action="delete" title="Delete">🗑</button>' : ''}
92
+ </div>
93
+ ` : ''}
94
+ </div>
95
+ `;
96
+ }).join('');
97
+ }
98
+
99
+ /**
100
+ * Prompt for satellite name (with fallback for browsers that block prompt)
101
+ */
102
+ export function promptForSatelliteName(onSuccess) {
103
+ const name = prompt('Name your new satellite:');
104
+ if (name && name.trim()) {
105
+ onSuccess(name.trim());
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Show/hide switching indicator overlay
111
+ */
112
+ export function showSwitchingIndicator(show) {
113
+ const messagesEl = document.getElementById('messages');
114
+ const inputArea = document.querySelector('.input-area, .chat-input');
115
+ const overlay = document.getElementById('satelliteSwitchOverlay');
116
+
117
+ if (show) {
118
+ // Disable input during switch
119
+ if (inputArea) {
120
+ inputArea.classList.add('switching-satellite');
121
+ inputArea.style.pointerEvents = 'none';
122
+ inputArea.style.opacity = '0.5';
123
+ }
124
+
125
+ // Add loading overlay to messages area
126
+ if (messagesEl && !overlay) {
127
+ const loadingOverlay = document.createElement('div');
128
+ loadingOverlay.id = 'satelliteSwitchOverlay';
129
+ loadingOverlay.className = 'satellite-switch-overlay';
130
+ loadingOverlay.innerHTML = `
131
+ <div class="satellite-switch-spinner">
132
+ <span>🛰️</span>
133
+ <span>Switching...</span>
134
+ </div>
135
+ `;
136
+ loadingOverlay.style.cssText = `
137
+ position: absolute;
138
+ top: 0;
139
+ left: 0;
140
+ right: 0;
141
+ bottom: 0;
142
+ background: rgba(0,0,0,0.3);
143
+ backdrop-filter: blur(4px);
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ z-index: 10;
148
+ `;
149
+ messagesEl.parentElement.style.position = 'relative';
150
+ messagesEl.parentElement.appendChild(loadingOverlay);
151
+ }
152
+ } else {
153
+ // Re-enable input
154
+ if (inputArea) {
155
+ inputArea.classList.remove('switching-satellite');
156
+ inputArea.style.pointerEvents = '';
157
+ inputArea.style.opacity = '';
158
+ }
159
+
160
+ // Remove overlay
161
+ if (overlay) {
162
+ overlay.remove();
163
+ }
164
+ }
165
+ }
166
+
167
+ // Expose on window for backward compatibility
168
+ if (typeof window !== 'undefined') {
169
+ window.UplinkSatelliteUI = {
170
+ showNotification,
171
+ buildSatelliteList,
172
+ promptForSatelliteName,
173
+ showSwitchingIndicator
174
+ };
175
+ }