@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,77 @@
1
+ // ============================================
2
+ // EVENT BUS MODULE
3
+ // Observer pattern for cross-module communication
4
+ // Replaces window.* patching pattern
5
+ // ============================================
6
+
7
+ const listeners = {};
8
+
9
+ /**
10
+ * Subscribe to an event
11
+ * @param {string} event - Event name
12
+ * @param {Function} callback - Event handler
13
+ * @returns {Function} Unsubscribe function
14
+ */
15
+ export function on(event, callback) {
16
+ if (!listeners[event]) listeners[event] = [];
17
+ listeners[event].push(callback);
18
+
19
+ // Return unsubscribe function
20
+ return () => off(event, callback);
21
+ }
22
+
23
+ /**
24
+ * Unsubscribe from an event
25
+ * @param {string} event - Event name
26
+ * @param {Function} callback - Event handler to remove
27
+ */
28
+ export function off(event, callback) {
29
+ if (!listeners[event]) return;
30
+ listeners[event] = listeners[event].filter(cb => cb !== callback);
31
+ }
32
+
33
+ /**
34
+ * Emit an event to all subscribers
35
+ * @param {string} event - Event name
36
+ * @param {*} data - Event data
37
+ */
38
+ export function emit(event, data) {
39
+ if (!listeners[event]) return;
40
+ listeners[event].forEach(cb => {
41
+ try {
42
+ cb(data);
43
+ } catch (err) {
44
+ console.error(`[EventBus] Error in listener for "${event}":`, err);
45
+ }
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Get count of listeners for an event (for debugging)
51
+ * @param {string} event - Event name
52
+ * @returns {number} Number of listeners
53
+ */
54
+ export function listenerCount(event) {
55
+ return listeners[event] ? listeners[event].length : 0;
56
+ }
57
+
58
+ /**
59
+ * Clear all listeners for an event
60
+ * @param {string} event - Event name (optional, clears all if omitted)
61
+ */
62
+ export function clear(event) {
63
+ if (event) {
64
+ delete listeners[event];
65
+ } else {
66
+ Object.keys(listeners).forEach(key => delete listeners[key]);
67
+ }
68
+ }
69
+
70
+ // Export singleton for backwards compatibility
71
+ const EventBus = { on, off, emit, listenerCount, clear };
72
+ export default EventBus;
73
+
74
+ // Also expose on window for debugging
75
+ if (typeof window !== 'undefined') {
76
+ window.UplinkEventBus = EventBus;
77
+ }
@@ -0,0 +1,171 @@
1
+ // ============================================
2
+ // FETCH UTILS MODULE
3
+ // Unified fetch wrapper with hooks for logging, timeouts, and metrics
4
+ // ============================================
5
+
6
+ import { UplinkLogger } from './logger.js';
7
+
8
+ // Store original fetch before any interception
9
+ const originalFetch = window.fetch;
10
+
11
+ // Hook registry
12
+ const hooks = {
13
+ beforeRequest: [], // (url, options) => modified options or void
14
+ afterResponse: [], // (url, options, response, duration) => void
15
+ onError: [] // (url, options, error, duration) => void
16
+ };
17
+
18
+ // Default timeout (30 seconds)
19
+ let defaultTimeout = 30000;
20
+
21
+ /**
22
+ * Register a hook
23
+ * @param {string} type - 'beforeRequest', 'afterResponse', or 'onError'
24
+ * @param {Function} fn - Hook function
25
+ * @returns {Function} Unregister function
26
+ */
27
+ function registerHook(type, fn) {
28
+ if (!hooks[type]) {
29
+ UplinkLogger.warn('FetchUtils: Unknown hook type', type);
30
+ return () => {};
31
+ }
32
+ hooks[type].push(fn);
33
+ return () => {
34
+ const idx = hooks[type].indexOf(fn);
35
+ if (idx > -1) hooks[type].splice(idx, 1);
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Set default timeout for all requests
41
+ * @param {number} ms - Timeout in milliseconds
42
+ */
43
+ function setFetchTimeout(ms) {
44
+ defaultTimeout = ms;
45
+ }
46
+
47
+ /**
48
+ * Enhanced fetch with timeout and hooks
49
+ * @param {string|Request} input - URL or Request object
50
+ * @param {Object} options - Fetch options (plus optional timeout)
51
+ * @returns {Promise<Response>}
52
+ */
53
+ async function enhancedFetch(input, options = {}) {
54
+ const url = typeof input === 'string' ? input : input.url;
55
+ const startTime = Date.now();
56
+
57
+ // Extract timeout from options or use default
58
+ const timeout = options.timeout ?? defaultTimeout;
59
+ const fetchOptions = { ...options };
60
+ delete fetchOptions.timeout;
61
+
62
+ // Run beforeRequest hooks
63
+ for (const hook of hooks.beforeRequest) {
64
+ try {
65
+ const modified = await hook(url, fetchOptions);
66
+ if (modified) Object.assign(fetchOptions, modified);
67
+ } catch (e) {
68
+ UplinkLogger.warn('FetchUtils: beforeRequest hook error', e);
69
+ }
70
+ }
71
+
72
+ // Create abort controller for timeout
73
+ const controller = new AbortController();
74
+ const existingSignal = fetchOptions.signal;
75
+ fetchOptions.signal = controller.signal;
76
+
77
+ // Set up timeout
78
+ let timeoutId = null;
79
+ if (timeout > 0) {
80
+ timeoutId = window.setTimeout(() => controller.abort(), timeout);
81
+ }
82
+
83
+ // Handle existing abort signal
84
+ if (existingSignal) {
85
+ existingSignal.addEventListener('abort', () => controller.abort());
86
+ }
87
+
88
+ try {
89
+ const response = await originalFetch(input, fetchOptions);
90
+ const duration = Date.now() - startTime;
91
+
92
+ // Clear timeout
93
+ if (timeoutId) window.clearTimeout(timeoutId);
94
+
95
+ // Run afterResponse hooks
96
+ for (const hook of hooks.afterResponse) {
97
+ try {
98
+ await hook(url, fetchOptions, response.clone(), duration);
99
+ } catch (e) {
100
+ UplinkLogger.warn('FetchUtils: afterResponse hook error', e);
101
+ }
102
+ }
103
+
104
+ return response;
105
+ } catch (error) {
106
+ const duration = Date.now() - startTime;
107
+
108
+ // Clear timeout
109
+ if (timeoutId) window.clearTimeout(timeoutId);
110
+
111
+ // Enhance timeout errors
112
+ if (error.name === 'AbortError' && duration >= timeout - 100) {
113
+ error.isTimeout = true;
114
+ error.message = `Request timeout after ${timeout}ms`;
115
+ }
116
+
117
+ // Run onError hooks
118
+ for (const hook of hooks.onError) {
119
+ try {
120
+ await hook(url, fetchOptions, error, duration);
121
+ } catch (e) {
122
+ UplinkLogger.warn('FetchUtils: onError hook error', e);
123
+ }
124
+ }
125
+
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Install the enhanced fetch globally
132
+ * Call this once after all hooks are registered
133
+ */
134
+ function install() {
135
+ window.fetch = enhancedFetch;
136
+ UplinkLogger.debug('FetchUtils: Installed enhanced fetch');
137
+ }
138
+
139
+ /**
140
+ * Restore original fetch
141
+ */
142
+ function uninstall() {
143
+ window.fetch = originalFetch;
144
+ UplinkLogger.debug('FetchUtils: Restored original fetch');
145
+ }
146
+
147
+ /**
148
+ * Get the original unmodified fetch
149
+ */
150
+ function getOriginalFetch() {
151
+ return originalFetch;
152
+ }
153
+
154
+ // Export API
155
+ export const UplinkFetch = {
156
+ registerHook,
157
+ setTimeout: setFetchTimeout,
158
+ install,
159
+ uninstall,
160
+ getOriginalFetch,
161
+ fetch: enhancedFetch
162
+ };
163
+
164
+ export { registerHook, setFetchTimeout, install, uninstall, getOriginalFetch, enhancedFetch };
165
+
166
+ // Backward compat: assign to window
167
+ window.UplinkFetch = UplinkFetch;
168
+
169
+ // Auto-install enhanced fetch
170
+ // Hooks can be registered before or after install - they're called dynamically
171
+ install();
@@ -0,0 +1,229 @@
1
+ // ============================================
2
+ // FILE HANDLER MODULE
3
+ // Image and file upload logic with progress tracking
4
+ // ============================================
5
+
6
+ import { UplinkLogger } from './logger.js';
7
+ import { UplinkSSEParser } from './utils/sse-parser.js';
8
+ import { UplinkErrors } from './errors.js';
9
+
10
+ // ============================================
11
+ // UPLOAD WITH PROGRESS (XHR)
12
+ // ============================================
13
+
14
+ export function uploadWithProgress(url, formData, onProgress) {
15
+ return new Promise((resolve, reject) => {
16
+ const xhr = new XMLHttpRequest();
17
+
18
+ xhr.upload.addEventListener('progress', (e) => {
19
+ if (e.lengthComputable && onProgress) {
20
+ const percentComplete = Math.round((e.loaded / e.total) * 100);
21
+ onProgress(percentComplete);
22
+ }
23
+ });
24
+
25
+ xhr.addEventListener('load', () => {
26
+ if (xhr.status >= 200 && xhr.status < 300) {
27
+ resolve({ ok: true, status: xhr.status, blob: xhr.response });
28
+ } else {
29
+ reject(new Error(`Upload failed: ${xhr.status}`));
30
+ }
31
+ });
32
+
33
+ xhr.addEventListener('error', () => reject(new Error('Network error during upload')));
34
+ xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));
35
+
36
+ xhr.open('POST', url);
37
+ xhr.responseType = 'blob';
38
+ xhr.send(formData);
39
+ });
40
+ }
41
+
42
+ async function blobToStreamResponse(blob) {
43
+ const text = await blob.text();
44
+ return {
45
+ body: new ReadableStream({
46
+ start(controller) {
47
+ controller.enqueue(new TextEncoder().encode(text));
48
+ controller.close();
49
+ }
50
+ })
51
+ };
52
+ }
53
+
54
+ // ============================================
55
+ // IMAGE UPLOAD
56
+ // ============================================
57
+
58
+ export async function sendImageMessage(imageUrl, caption, chat, skipAddMessage, imageTypingTimeoutMs) {
59
+ caption = caption || '';
60
+ imageTypingTimeoutMs = imageTypingTimeoutMs || 300000;
61
+
62
+ if (!skipAddMessage) chat.addMessage(caption, 'user', imageUrl);
63
+
64
+ const progressMsg = chat.addMessage('Uploading image...', 'system', null, false);
65
+ chat.showTyping(imageTypingTimeoutMs);
66
+
67
+ try {
68
+ const blobResponse = await fetch(imageUrl);
69
+ const blob = await blobResponse.blob();
70
+
71
+ const form = new FormData();
72
+ form.append('image', blob, 'image.jpg');
73
+ if (caption) form.append('caption', caption);
74
+
75
+ const uploadBlob = await uploadWithProgress('/api/image', form, (pct) => {
76
+ if (progressMsg && progressMsg.parentNode) {
77
+ const progressText = progressMsg.querySelector('.message-content');
78
+ if (progressText) {
79
+ progressText.textContent = `Uploading image... ${pct}%`;
80
+ }
81
+ }
82
+ });
83
+
84
+ if (progressMsg && progressMsg.parentNode) {
85
+ progressMsg.remove();
86
+ }
87
+
88
+ const streamResponse = await blobToStreamResponse(uploadBlob.blob);
89
+
90
+ let uploadResult = null;
91
+ await UplinkSSEParser.processStream(streamResponse, {
92
+ onData: (payload) => { uploadResult = payload; },
93
+ onError: (err) => {
94
+ UplinkLogger.warn('[Image Upload] SSE parse error:', err);
95
+ }
96
+ });
97
+
98
+ chat.hideTyping();
99
+
100
+ if (uploadResult?.response) {
101
+ if (uploadResult.imageUrl) {
102
+ chat.updateLastUserImageUrl(uploadResult.imageUrl);
103
+ }
104
+ chat.addMessage(uploadResult.response, 'assistant');
105
+ if (window.UplinkConnection?.markMessageSeen) {
106
+ window.UplinkConnection.markMessageSeen(null, 'assistant', uploadResult.response, Date.now());
107
+ }
108
+ const core = window.UplinkCore;
109
+ if (core?.audioResponses) {
110
+ const urls = uploadResult.audioUrls?.length
111
+ ? uploadResult.audioUrls
112
+ : (uploadResult.audioUrl ? [uploadResult.audioUrl] : []);
113
+ urls.forEach(url => chat.playAudio(url));
114
+ }
115
+ } else if (uploadResult?.error) {
116
+ const friendlyMsg = UplinkErrors.getFriendlyMessage(uploadResult.error) || uploadResult.error;
117
+ chat.addMessage(friendlyMsg, 'system', null, false);
118
+ } else {
119
+ chat.addMessage('No response received from image analysis', 'system', null, false);
120
+ }
121
+ } catch (uploadError) {
122
+ chat.hideTyping();
123
+ const friendlyMsg = UplinkErrors.getFriendlyMessage(uploadError) || 'Upload failed';
124
+ chat.addMessage(friendlyMsg, 'system', null, false);
125
+ }
126
+ }
127
+
128
+ // ============================================
129
+ // FILE UPLOAD (non-image)
130
+ // ============================================
131
+
132
+ export async function sendFileMessage(fileInfo, caption, chat, imageTypingTimeoutMs) {
133
+ caption = caption || '';
134
+ imageTypingTimeoutMs = imageTypingTimeoutMs || 300000;
135
+
136
+ const icon = window.UplinkFiles?.getFileIcon?.(fileInfo.name) || '📎';
137
+ chat.addMessage(`${icon} ${fileInfo.name}${caption ? ': ' + caption : ''}`, 'user');
138
+
139
+ const progressMsg = chat.addMessage('Uploading file...', 'system', null, false);
140
+ chat.showTyping(imageTypingTimeoutMs);
141
+
142
+ try {
143
+ const form = new FormData();
144
+ form.append('file', fileInfo.blob, fileInfo.name);
145
+ if (caption) form.append('caption', caption);
146
+
147
+ const satelliteId = window.UplinkSatellites?.getCurrentSatellite() || 'main';
148
+ form.append('satelliteId', satelliteId);
149
+
150
+ const uploadBlob = await uploadWithProgress('/api/file', form, (pct) => {
151
+ if (progressMsg?.parentNode) {
152
+ const progressText = progressMsg.querySelector('.message-content');
153
+ if (progressText) progressText.textContent = `Uploading file... ${pct}%`;
154
+ }
155
+ });
156
+
157
+ if (progressMsg?.parentNode) progressMsg.remove();
158
+
159
+ const streamResponse = await blobToStreamResponse(uploadBlob.blob);
160
+
161
+ let result = null;
162
+ await UplinkSSEParser.processStream(streamResponse, {
163
+ onData: (payload) => { result = payload; },
164
+ onError: (err) => {
165
+ UplinkLogger.warn('[File Upload] SSE parse error:', err);
166
+ }
167
+ });
168
+
169
+ chat.hideTyping();
170
+
171
+ if (result?.response) {
172
+ chat.addMessage(result.response, 'assistant');
173
+ if (window.UplinkConnection?.markMessageSeen) {
174
+ window.UplinkConnection.markMessageSeen(null, 'assistant', result.response, Date.now());
175
+ }
176
+ } else if (result?.error) {
177
+ const friendlyMsg = UplinkErrors.getFriendlyMessage(result.error) || result.error;
178
+ chat.addMessage(friendlyMsg, 'system', null, false);
179
+ } else {
180
+ chat.addMessage('No response received for file upload', 'system', null, false);
181
+ }
182
+ } catch (err) {
183
+ chat.hideTyping();
184
+ if (progressMsg?.parentNode) progressMsg.remove();
185
+ const friendlyMsg = UplinkErrors.getFriendlyMessage(err) || 'File upload failed';
186
+ chat.addMessage(friendlyMsg, 'system', null, false);
187
+ }
188
+ }
189
+
190
+ // ============================================
191
+ // IMAGE URL UPDATE
192
+ // ============================================
193
+
194
+ export function updateLastUserImageUrl(container, serverUrl) {
195
+ if (!container) return;
196
+
197
+ const userMsgs = container.querySelectorAll('.message.user');
198
+ for (let i = userMsgs.length - 1; i >= 0; i--) {
199
+ const img = userMsgs[i].querySelector('img');
200
+ if (img && (img.src.startsWith('data:') || img.src.includes('__pending_upload__'))) {
201
+ img.src = serverUrl;
202
+
203
+ if (window.UplinkStorage) {
204
+ window.UplinkStorage.updateLastImageUrl(serverUrl);
205
+ }
206
+
207
+ if (window.UplinkSatellites?.updateLastImageUrl) {
208
+ window.UplinkSatellites.updateLastImageUrl(serverUrl);
209
+ }
210
+ break;
211
+ }
212
+ }
213
+ }
214
+
215
+ // ============================================
216
+ // PUBLIC API
217
+ // ============================================
218
+
219
+ export const UplinkFileHandler = {
220
+ sendImageMessage,
221
+ sendFileMessage,
222
+ updateLastUserImageUrl,
223
+ uploadWithProgress
224
+ };
225
+
226
+ // Backward compat: assign to window
227
+ window.UplinkFileHandler = UplinkFileHandler;
228
+
229
+ UplinkLogger.debug('FileHandler: Module loaded');