@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.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,1516 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// SATELLITES MODULE
|
|
3
|
+
// Multiple independent conversation tabs
|
|
4
|
+
// ============================================
|
|
5
|
+
//
|
|
6
|
+
// DEPENDENCY INTERFACE:
|
|
7
|
+
// This module requires the following dependencies, which can be provided
|
|
8
|
+
// via UplinkSatellites.init(config) or will fall back to window.* globals:
|
|
9
|
+
//
|
|
10
|
+
// - chat: { onConnection(cb), isConnected(), connected? } - Connection status
|
|
11
|
+
// - notifications: { show(message, type) } - User notifications
|
|
12
|
+
// - storage: { loadHistory() } - For migration from legacy storage
|
|
13
|
+
// - panels: { register(id, handlers), toggle(id) } - Panel management
|
|
14
|
+
// - core: { registerModule(name, initFn) } - Module lifecycle
|
|
15
|
+
// - messageApi: { addMessage(text, type, imageUrl, save) } - Chat messages
|
|
16
|
+
// - logger: { debug(...), warn(...), error(...) } - Logging
|
|
17
|
+
//
|
|
18
|
+
// EVENTS EMITTED:
|
|
19
|
+
// - 'uplink:satellite-switching' - Before switch (detail: { satelliteId })
|
|
20
|
+
// - 'uplink:satellite-switched' - After switch (detail: { satelliteId })
|
|
21
|
+
// - 'satellite:switched' (via event bus) - After switch (detail: { satelliteId })
|
|
22
|
+
//
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
import { emit as emitEvent, on as onEvent } from './event-bus.js';
|
|
26
|
+
import * as SatelliteUI from './satellite-ui.js';
|
|
27
|
+
|
|
28
|
+
const STORAGE_KEY = 'uplink-satellites';
|
|
29
|
+
const PRIMARY_SATELLITE = 'main';
|
|
30
|
+
|
|
31
|
+
// ============================================
|
|
32
|
+
// DEPENDENCY CONTAINER
|
|
33
|
+
// ============================================
|
|
34
|
+
|
|
35
|
+
// Dependencies with fallbacks to window.* for backwards compatibility
|
|
36
|
+
const deps = {
|
|
37
|
+
chat: null,
|
|
38
|
+
notifications: null,
|
|
39
|
+
storage: null,
|
|
40
|
+
panels: null,
|
|
41
|
+
core: null,
|
|
42
|
+
messageApi: null,
|
|
43
|
+
logger: null
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a dependency - use injected or fall back to window.*
|
|
48
|
+
*/
|
|
49
|
+
function getChat() {
|
|
50
|
+
return deps.chat || window.gatewayChat || window.UplinkChat || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getNotifications() {
|
|
54
|
+
return deps.notifications || window.UplinkNotifications || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getStorage() {
|
|
58
|
+
return deps.storage || window.UplinkStorage || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getPanels() {
|
|
62
|
+
return deps.panels || window.UplinkPanels || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getCore() {
|
|
66
|
+
return deps.core || window.UplinkCore || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getLogger() {
|
|
70
|
+
return deps.logger || window.logger || console;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getMessageApi() {
|
|
74
|
+
// For message API, prefer UplinkChat's addMessage
|
|
75
|
+
if (deps.messageApi) return deps.messageApi;
|
|
76
|
+
if (window.UplinkChat && window.UplinkChat.addMessage) {
|
|
77
|
+
return { addMessage: window.UplinkChat.addMessage };
|
|
78
|
+
}
|
|
79
|
+
if (window._originalAddMessage) {
|
|
80
|
+
return { addMessage: window._originalAddMessage };
|
|
81
|
+
}
|
|
82
|
+
if (window.addMessage) {
|
|
83
|
+
return { addMessage: window.addMessage };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================
|
|
89
|
+
// STATE
|
|
90
|
+
// ============================================
|
|
91
|
+
|
|
92
|
+
let satellites = {};
|
|
93
|
+
let currentSatellite = PRIMARY_SATELLITE;
|
|
94
|
+
let defaultSatellite = PRIMARY_SATELLITE; // Pinned to top of list
|
|
95
|
+
let panelVisible = false;
|
|
96
|
+
|
|
97
|
+
// UI Elements
|
|
98
|
+
let navigatorPanel = null;
|
|
99
|
+
|
|
100
|
+
// Timer tracking for cleanup
|
|
101
|
+
let connectionPollInterval = null;
|
|
102
|
+
let pendingDeleteTimers = new Map();
|
|
103
|
+
|
|
104
|
+
// History fetch debouncing (for visibility change)
|
|
105
|
+
let lastHistoryFetch = 0;
|
|
106
|
+
const HISTORY_FETCH_DEBOUNCE_MS = 10000; // 10 seconds
|
|
107
|
+
|
|
108
|
+
// Module init retry state
|
|
109
|
+
let initRetryCount = 0;
|
|
110
|
+
const MAX_INIT_RETRIES = 10;
|
|
111
|
+
|
|
112
|
+
// Track if message hook is installed
|
|
113
|
+
let messageHookInstalled = false;
|
|
114
|
+
|
|
115
|
+
// ============================================
|
|
116
|
+
// INITIALIZATION
|
|
117
|
+
// ============================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Initialize the satellites module with optional dependency injection
|
|
121
|
+
* @param {Object} config - Optional configuration object
|
|
122
|
+
* @param {Object} config.chat - Chat module with onConnection, isConnected
|
|
123
|
+
* @param {Object} config.notifications - Notifications module with show()
|
|
124
|
+
* @param {Object} config.storage - Storage module with loadHistory()
|
|
125
|
+
* @param {Object} config.panels - Panel manager with register(), toggle()
|
|
126
|
+
* @param {Object} config.core - Core module with registerModule()
|
|
127
|
+
* @param {Object} config.messageApi - Message API with addMessage()
|
|
128
|
+
* @param {Object} config.logger - Logger with debug, warn, error
|
|
129
|
+
*/
|
|
130
|
+
async function init(config = {}) {
|
|
131
|
+
// Inject dependencies if provided
|
|
132
|
+
if (config.chat) deps.chat = config.chat;
|
|
133
|
+
if (config.notifications) deps.notifications = config.notifications;
|
|
134
|
+
if (config.storage) deps.storage = config.storage;
|
|
135
|
+
if (config.panels) deps.panels = config.panels;
|
|
136
|
+
if (config.core) deps.core = config.core;
|
|
137
|
+
if (config.messageApi) deps.messageApi = config.messageApi;
|
|
138
|
+
if (config.logger) deps.logger = config.logger;
|
|
139
|
+
|
|
140
|
+
loadSatellites();
|
|
141
|
+
await syncRemoteSessionsWrapper();
|
|
142
|
+
createNavigatorPanel();
|
|
143
|
+
injectMessageHooks();
|
|
144
|
+
|
|
145
|
+
// Also listen to event bus for messages (in addition to hook)
|
|
146
|
+
onEvent('message:added', ({ text, type, imageUrl, save }) => {
|
|
147
|
+
if (save && type !== 'system') {
|
|
148
|
+
addMessageToSatellite({ text, type, imageUrl });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Handle URL parameter for satellite embedding (e.g., ?satellite=redos-workshop)
|
|
153
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
154
|
+
const urlSatelliteId = urlParams.get('satellite');
|
|
155
|
+
if (urlSatelliteId && urlSatelliteId !== 'main') {
|
|
156
|
+
const sanitizedId = urlSatelliteId.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 32);
|
|
157
|
+
if (sanitizedId) {
|
|
158
|
+
// Create satellite if it doesn't exist
|
|
159
|
+
if (!satellites[sanitizedId]) {
|
|
160
|
+
// Use URL param 'name' if provided, otherwise humanize the ID
|
|
161
|
+
const urlSatelliteName = urlParams.get('name') || sanitizedId.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
162
|
+
satellites[sanitizedId] = {
|
|
163
|
+
name: urlSatelliteName,
|
|
164
|
+
createdAt: Date.now(),
|
|
165
|
+
embedded: true // Mark as externally embedded
|
|
166
|
+
};
|
|
167
|
+
saveSatellites();
|
|
168
|
+
getLogger().debug('Satellites: Created embedded satellite from URL:', sanitizedId, urlSatelliteName);
|
|
169
|
+
}
|
|
170
|
+
// Switch to the satellite
|
|
171
|
+
currentSatellite = sanitizedId;
|
|
172
|
+
getLogger().debug('Satellites: Switched to URL satellite:', sanitizedId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Migrate existing history if this is first run
|
|
177
|
+
await migrateExistingHistory();
|
|
178
|
+
|
|
179
|
+
// Check session status on init
|
|
180
|
+
updateSessionStatus();
|
|
181
|
+
|
|
182
|
+
// Setup connection listener for history loading with race condition protection
|
|
183
|
+
setupConnectionListener();
|
|
184
|
+
|
|
185
|
+
// Setup visibility change handler for history refresh on tab focus
|
|
186
|
+
setupVisibilityHandler();
|
|
187
|
+
|
|
188
|
+
getLogger().debug('Satellites: Initialized with', Object.keys(satellites).length, 'satellites');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================
|
|
192
|
+
// CONNECTION LISTENER (Race Condition Fix)
|
|
193
|
+
// ============================================
|
|
194
|
+
|
|
195
|
+
function setupConnectionListener() {
|
|
196
|
+
// Try UplinkConnection first (handles WebSocket state)
|
|
197
|
+
const connection = window.UplinkConnection;
|
|
198
|
+
const chat = getChat();
|
|
199
|
+
|
|
200
|
+
console.log('[Satellites] setupConnectionListener - connection:', !!connection, 'chat:', !!chat);
|
|
201
|
+
|
|
202
|
+
// Prefer UplinkConnection for connection events
|
|
203
|
+
if (connection && connection.onConnection) {
|
|
204
|
+
console.log('[Satellites] Using UplinkConnection for connection events');
|
|
205
|
+
connection.onConnection((event) => {
|
|
206
|
+
if (event === 'connected') {
|
|
207
|
+
console.log('[Satellites] UplinkConnection: connected event, loading history');
|
|
208
|
+
loadHistoryOnConnect();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Check if already connected
|
|
213
|
+
if (connection.isConnected && connection.isConnected()) {
|
|
214
|
+
console.log('[Satellites] UplinkConnection: already connected, loading history');
|
|
215
|
+
loadHistoryOnConnect();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
} else if (chat && chat.onConnection) {
|
|
219
|
+
// Fallback to chat module
|
|
220
|
+
chat.onConnection(() => {
|
|
221
|
+
console.log('[Satellites] Connection callback fired, loading history');
|
|
222
|
+
loadHistoryOnConnect();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (chat.isConnected && chat.isConnected()) {
|
|
226
|
+
console.log('[Satellites] Already connected on init, loading history');
|
|
227
|
+
loadHistoryOnConnect();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
console.log('[Satellites] No connection module found, will use polling fallback');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Polling fallback: Check every 100ms for connection readiness
|
|
235
|
+
let pollCount = 0;
|
|
236
|
+
const maxPolls = 50; // 5 seconds max
|
|
237
|
+
|
|
238
|
+
// Clear any existing poll interval before starting new one
|
|
239
|
+
if (connectionPollInterval) {
|
|
240
|
+
clearInterval(connectionPollInterval);
|
|
241
|
+
connectionPollInterval = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
connectionPollInterval = setInterval(() => {
|
|
245
|
+
pollCount++;
|
|
246
|
+
|
|
247
|
+
const conn = window.UplinkConnection;
|
|
248
|
+
const chatInstance = getChat();
|
|
249
|
+
|
|
250
|
+
if (pollCount === 1 || pollCount % 10 === 0) {
|
|
251
|
+
const connStatus = conn ? (conn.isConnected ? conn.isConnected() : 'no isConnected') : 'no conn';
|
|
252
|
+
console.log('[Satellites] Polling attempt', pollCount, '- connection:', connStatus);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check UplinkConnection first
|
|
256
|
+
if (conn && conn.isConnected && conn.isConnected()) {
|
|
257
|
+
console.log('[Satellites] Connection detected via UplinkConnection polling');
|
|
258
|
+
clearInterval(connectionPollInterval);
|
|
259
|
+
connectionPollInterval = null;
|
|
260
|
+
loadHistoryOnConnect();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Fallback: check chat instance
|
|
265
|
+
if (chatInstance) {
|
|
266
|
+
const isConnected = chatInstance.isConnected ? chatInstance.isConnected() :
|
|
267
|
+
chatInstance.connected || chatInstance.readyState === 1;
|
|
268
|
+
|
|
269
|
+
if (isConnected) {
|
|
270
|
+
console.log('[Satellites] Connection detected via chat polling');
|
|
271
|
+
clearInterval(connectionPollInterval);
|
|
272
|
+
connectionPollInterval = null;
|
|
273
|
+
loadHistoryOnConnect();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Stop polling after max attempts
|
|
279
|
+
if (pollCount >= maxPolls) {
|
|
280
|
+
clearInterval(connectionPollInterval);
|
|
281
|
+
connectionPollInterval = null;
|
|
282
|
+
console.log('[Satellites] Connection polling timed out after', maxPolls, 'attempts');
|
|
283
|
+
// Last resort: try loading history anyway (HTTP fetch might still work)
|
|
284
|
+
console.log('[Satellites] Attempting history load despite no connection detected');
|
|
285
|
+
loadHistoryOnConnect();
|
|
286
|
+
}
|
|
287
|
+
}, 100);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function loadHistoryOnConnect() {
|
|
291
|
+
console.log('[Satellites] loadHistoryOnConnect called');
|
|
292
|
+
|
|
293
|
+
// Fetch history from Gateway and merge with local messages
|
|
294
|
+
await fetchAndMergeGatewayHistory();
|
|
295
|
+
|
|
296
|
+
// Reload chat display for the current satellite
|
|
297
|
+
reloadChatDisplay();
|
|
298
|
+
|
|
299
|
+
// Also refresh session status
|
|
300
|
+
updateSessionStatus();
|
|
301
|
+
|
|
302
|
+
// Track fetch time for debouncing
|
|
303
|
+
lastHistoryFetch = Date.now();
|
|
304
|
+
|
|
305
|
+
console.log('[Satellites] loadHistoryOnConnect complete');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Handle visibility change - fetch history when tab becomes visible
|
|
310
|
+
*/
|
|
311
|
+
function setupVisibilityHandler() {
|
|
312
|
+
document.addEventListener('visibilitychange', async () => {
|
|
313
|
+
if (document.visibilityState !== 'visible') return;
|
|
314
|
+
|
|
315
|
+
// Don't fire API calls if not connected yet (avoid fetch errors during reconnect)
|
|
316
|
+
if (!window.UplinkConnection?.isConnected?.()) {
|
|
317
|
+
getLogger().debug('Satellites: Skipping visibility fetch (not connected)');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Debounce: don't fetch if we just fetched recently
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
if (now - lastHistoryFetch < HISTORY_FETCH_DEBOUNCE_MS) {
|
|
324
|
+
getLogger().debug('Satellites: Skipping visibility fetch (debounced)');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getLogger().debug('Satellites: Tab visible, fetching history...');
|
|
329
|
+
lastHistoryFetch = now;
|
|
330
|
+
|
|
331
|
+
// Sync remote sessions (pick up satellites created on other devices)
|
|
332
|
+
const prevCount = Object.keys(satellites).length;
|
|
333
|
+
await syncRemoteSessionsWrapper();
|
|
334
|
+
if (Object.keys(satellites).length > prevCount) {
|
|
335
|
+
// New satellites appeared — refresh the navigator panel
|
|
336
|
+
createNavigatorPanel();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await fetchAndMergeGatewayHistory();
|
|
340
|
+
reloadChatDisplay();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Fetch history from Gateway and merge with local satellite messages
|
|
346
|
+
*/
|
|
347
|
+
async function fetchAndMergeGatewayHistory() {
|
|
348
|
+
if (!window.UplinkSatelliteSync) {
|
|
349
|
+
getLogger().warn('Satellites: UplinkSatelliteSync not loaded yet');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const satellite = satellites[currentSatellite];
|
|
354
|
+
if (!satellite) return;
|
|
355
|
+
|
|
356
|
+
await window.UplinkSatelliteSync.fetchAndMergeGatewayHistory(currentSatellite, satellite);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ============================================
|
|
360
|
+
// DATA MANAGEMENT
|
|
361
|
+
// ============================================
|
|
362
|
+
|
|
363
|
+
function loadSatellites() {
|
|
364
|
+
try {
|
|
365
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
366
|
+
if (stored) {
|
|
367
|
+
const data = JSON.parse(stored);
|
|
368
|
+
satellites = data.satellites || {};
|
|
369
|
+
currentSatellite = data.currentSatellite || PRIMARY_SATELLITE;
|
|
370
|
+
defaultSatellite = data.defaultSatellite || PRIMARY_SATELLITE;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Ensure primary satellite exists
|
|
374
|
+
if (!satellites[PRIMARY_SATELLITE]) {
|
|
375
|
+
satellites[PRIMARY_SATELLITE] = createSatelliteData(PRIMARY_SATELLITE, 'Primary', null);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Ensure default satellite still exists, fallback to primary
|
|
379
|
+
if (!satellites[defaultSatellite]) {
|
|
380
|
+
defaultSatellite = PRIMARY_SATELLITE;
|
|
381
|
+
}
|
|
382
|
+
} catch (e) {
|
|
383
|
+
getLogger().error('Satellites: Failed to load', e);
|
|
384
|
+
satellites = { [PRIMARY_SATELLITE]: createSatelliteData(PRIMARY_SATELLITE, 'Primary', null) };
|
|
385
|
+
currentSatellite = PRIMARY_SATELLITE;
|
|
386
|
+
defaultSatellite = PRIMARY_SATELLITE;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function saveSatellites() {
|
|
391
|
+
try {
|
|
392
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
393
|
+
satellites,
|
|
394
|
+
currentSatellite,
|
|
395
|
+
defaultSatellite
|
|
396
|
+
}));
|
|
397
|
+
} catch (e) {
|
|
398
|
+
getLogger().error('Satellites: Failed to save', e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Wrapper for sync module - maintains local state
|
|
404
|
+
*/
|
|
405
|
+
async function syncRemoteSessionsWrapper() {
|
|
406
|
+
if (!window.UplinkSatelliteSync) {
|
|
407
|
+
getLogger().warn('Satellites: UplinkSatelliteSync not loaded yet');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const result = await window.UplinkSatelliteSync.syncRemoteSessions(
|
|
412
|
+
satellites,
|
|
413
|
+
PRIMARY_SATELLITE,
|
|
414
|
+
currentSatellite
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Update local state from sync result
|
|
418
|
+
satellites = result.satellites;
|
|
419
|
+
currentSatellite = result.currentSatellite;
|
|
420
|
+
|
|
421
|
+
if (result.added > 0 || result.removed > 0) {
|
|
422
|
+
saveSatellites();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function createSatelliteData(id, name, agentId) {
|
|
427
|
+
return {
|
|
428
|
+
id,
|
|
429
|
+
name,
|
|
430
|
+
agentId: agentId || 'main',
|
|
431
|
+
createdAt: Date.now(),
|
|
432
|
+
messages: []
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function generateSatelliteId() {
|
|
437
|
+
return 'sat-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 4);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getAgentEmoji(agentId) {
|
|
441
|
+
const agents = window.UplinkAgents?.getAgents?.() || [];
|
|
442
|
+
const agent = agents.find(a => a.id === agentId);
|
|
443
|
+
return agent?.identity?.emoji || '🤖';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ============================================
|
|
447
|
+
// SATELLITE OPERATIONS
|
|
448
|
+
// ============================================
|
|
449
|
+
|
|
450
|
+
function promptForSatelliteName() {
|
|
451
|
+
// Create inline prompt in the navigator panel
|
|
452
|
+
const listEl = document.getElementById('satelliteList');
|
|
453
|
+
if (!listEl) return;
|
|
454
|
+
|
|
455
|
+
// Check if prompt already exists
|
|
456
|
+
if (listEl.querySelector('.satellite-name-prompt')) return;
|
|
457
|
+
|
|
458
|
+
// Build agent options from the agents module
|
|
459
|
+
const agentsList = window.UplinkAgents?.getAgents?.() || [];
|
|
460
|
+
const hasMultipleAgents = agentsList.length > 1;
|
|
461
|
+
const agentOptions = agentsList.map(a => {
|
|
462
|
+
const label = a.identity?.emoji ? `${a.identity.emoji} ${a.identity?.name || a.name || a.id}` : (a.identity?.name || a.name || a.id);
|
|
463
|
+
return `<option value="${a.id}" ${a.default ? 'selected' : ''}>${label}</option>`;
|
|
464
|
+
}).join('');
|
|
465
|
+
|
|
466
|
+
const promptHtml = `
|
|
467
|
+
<div class="satellite-name-prompt">
|
|
468
|
+
<input type="text" class="satellite-name-input" placeholder="Satellite name..." maxlength="32" autofocus>
|
|
469
|
+
${hasMultipleAgents ? `
|
|
470
|
+
<div class="satellite-agent-field">
|
|
471
|
+
<label class="satellite-agent-label">Agent</label>
|
|
472
|
+
<select class="satellite-agent-select" id="satelliteAgentSelect">
|
|
473
|
+
${agentOptions}
|
|
474
|
+
</select>
|
|
475
|
+
</div>
|
|
476
|
+
` : ''}
|
|
477
|
+
<div class="satellite-prompt-buttons">
|
|
478
|
+
<button class="satellite-prompt-cancel">Cancel</button>
|
|
479
|
+
<button class="satellite-prompt-create">Create</button>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
`;
|
|
483
|
+
|
|
484
|
+
listEl.insertAdjacentHTML('afterbegin', promptHtml);
|
|
485
|
+
|
|
486
|
+
const promptEl = listEl.querySelector('.satellite-name-prompt');
|
|
487
|
+
const inputEl = promptEl.querySelector('.satellite-name-input');
|
|
488
|
+
const cancelBtn = promptEl.querySelector('.satellite-prompt-cancel');
|
|
489
|
+
const createBtn = promptEl.querySelector('.satellite-prompt-create');
|
|
490
|
+
const agentSelect = promptEl.querySelector('#satelliteAgentSelect');
|
|
491
|
+
|
|
492
|
+
inputEl.focus();
|
|
493
|
+
|
|
494
|
+
const cleanup = () => promptEl.remove();
|
|
495
|
+
|
|
496
|
+
const doCreate = () => {
|
|
497
|
+
const name = inputEl.value.trim();
|
|
498
|
+
const agentId = agentSelect?.value || 'main';
|
|
499
|
+
cleanup();
|
|
500
|
+
launchSatellite(name || null, null, agentId);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
cancelBtn.addEventListener('click', cleanup);
|
|
504
|
+
createBtn.addEventListener('click', doCreate);
|
|
505
|
+
inputEl.addEventListener('keydown', (e) => {
|
|
506
|
+
if (e.key === 'Enter') doCreate();
|
|
507
|
+
if (e.key === 'Escape') cleanup();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function launchSatellite(name, initialMessages = null, agentId = null) {
|
|
512
|
+
const satId = generateSatelliteId();
|
|
513
|
+
const satellite = createSatelliteData(
|
|
514
|
+
satId,
|
|
515
|
+
name || `Satellite ${Object.keys(satellites).length}`,
|
|
516
|
+
agentId
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// If initial messages provided (fork), populate the satellite
|
|
520
|
+
if (initialMessages && Array.isArray(initialMessages)) {
|
|
521
|
+
satellite.messages = initialMessages.map(msg => ({
|
|
522
|
+
text: msg.text,
|
|
523
|
+
type: msg.type,
|
|
524
|
+
timestamp: Date.now()
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
satellites[satId] = satellite;
|
|
529
|
+
saveSatellites();
|
|
530
|
+
|
|
531
|
+
// Switch to new satellite
|
|
532
|
+
connectToSatellite(satId);
|
|
533
|
+
|
|
534
|
+
// Show notification
|
|
535
|
+
const forkNote = initialMessages ? ` (forked with ${initialMessages.length} messages)` : '';
|
|
536
|
+
showNotification(`Launched "${satellite.name}" 🛰️${forkNote}`);
|
|
537
|
+
|
|
538
|
+
return satId;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function connectToSatellite(satId) {
|
|
542
|
+
console.log('[Satellites] connectToSatellite called with:', satId);
|
|
543
|
+
if (!satellites[satId]) return false;
|
|
544
|
+
|
|
545
|
+
// Show loading state and disable interaction
|
|
546
|
+
showSwitchingIndicator(true);
|
|
547
|
+
|
|
548
|
+
currentSatellite = satId;
|
|
549
|
+
saveSatellites();
|
|
550
|
+
|
|
551
|
+
// Emit event BEFORE reload (so listeners can clear state like reply context)
|
|
552
|
+
window.dispatchEvent(new CustomEvent('uplink:satellite-switching', {
|
|
553
|
+
detail: { satelliteId: satId }
|
|
554
|
+
}));
|
|
555
|
+
|
|
556
|
+
// Clear sync deduplication state for new satellite
|
|
557
|
+
if (window.UplinkConnection?.clearSyncDedup) {
|
|
558
|
+
window.UplinkConnection.clearSyncDedup();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Fetch history from gateway then reload display
|
|
562
|
+
fetchAndMergeGatewayHistory().then(() => {
|
|
563
|
+
reloadChatDisplay();
|
|
564
|
+
});
|
|
565
|
+
updateNavigator();
|
|
566
|
+
|
|
567
|
+
// Update session status indicator
|
|
568
|
+
updateSessionStatus(satId);
|
|
569
|
+
|
|
570
|
+
// Show toast notification on satellite switch
|
|
571
|
+
showNotification(`Switched to "${satellites[satId].name}" 🛰️`);
|
|
572
|
+
|
|
573
|
+
// Emit event after switch complete
|
|
574
|
+
window.dispatchEvent(new CustomEvent('uplink:satellite-switched', {
|
|
575
|
+
detail: { satelliteId: satId }
|
|
576
|
+
}));
|
|
577
|
+
|
|
578
|
+
// Also emit via event bus for cross-module communication
|
|
579
|
+
emitEvent('satellite:switched', { satelliteId: satId });
|
|
580
|
+
|
|
581
|
+
// Hide loading state after a brief delay to ensure UI updates complete
|
|
582
|
+
setTimeout(() => {
|
|
583
|
+
showSwitchingIndicator(false);
|
|
584
|
+
}, 150);
|
|
585
|
+
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Show/hide loading indicator during satellite switch
|
|
591
|
+
*/
|
|
592
|
+
function showSwitchingIndicator(show) {
|
|
593
|
+
const messagesEl = document.getElementById('messages');
|
|
594
|
+
const inputArea = document.querySelector('.input-area, .chat-input');
|
|
595
|
+
const overlay = document.getElementById('satelliteSwitchOverlay');
|
|
596
|
+
|
|
597
|
+
if (show) {
|
|
598
|
+
// Disable input during switch
|
|
599
|
+
if (inputArea) {
|
|
600
|
+
inputArea.classList.add('switching-satellite');
|
|
601
|
+
inputArea.style.pointerEvents = 'none';
|
|
602
|
+
inputArea.style.opacity = '0.5';
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Add loading overlay to messages area
|
|
606
|
+
if (messagesEl && !overlay) {
|
|
607
|
+
const loadingOverlay = document.createElement('div');
|
|
608
|
+
loadingOverlay.id = 'satelliteSwitchOverlay';
|
|
609
|
+
loadingOverlay.className = 'satellite-switch-overlay';
|
|
610
|
+
loadingOverlay.innerHTML = `
|
|
611
|
+
<div class="satellite-switch-spinner">
|
|
612
|
+
<span>🛰️</span>
|
|
613
|
+
<span>Switching...</span>
|
|
614
|
+
</div>
|
|
615
|
+
`;
|
|
616
|
+
loadingOverlay.style.cssText = `
|
|
617
|
+
position: absolute;
|
|
618
|
+
top: 0;
|
|
619
|
+
left: 0;
|
|
620
|
+
right: 0;
|
|
621
|
+
bottom: 0;
|
|
622
|
+
background: var(--bg-primary, rgba(0,0,0,0.7));
|
|
623
|
+
display: flex;
|
|
624
|
+
align-items: center;
|
|
625
|
+
justify-content: center;
|
|
626
|
+
z-index: 100;
|
|
627
|
+
animation: fadeIn 0.15s ease;
|
|
628
|
+
`;
|
|
629
|
+
messagesEl.style.position = 'relative';
|
|
630
|
+
messagesEl.appendChild(loadingOverlay);
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
// Re-enable input
|
|
634
|
+
if (inputArea) {
|
|
635
|
+
inputArea.classList.remove('switching-satellite');
|
|
636
|
+
inputArea.style.pointerEvents = '';
|
|
637
|
+
inputArea.style.opacity = '';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Remove overlay
|
|
641
|
+
const existingOverlay = document.getElementById('satelliteSwitchOverlay');
|
|
642
|
+
if (existingOverlay) {
|
|
643
|
+
existingOverlay.remove();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Fetch and display session status from OpenClaw
|
|
650
|
+
*/
|
|
651
|
+
async function updateSessionStatus(satId = currentSatellite) {
|
|
652
|
+
const indicator = document.getElementById('sessionIndicator');
|
|
653
|
+
const keyDisplay = document.getElementById('sessionKeyDisplay');
|
|
654
|
+
|
|
655
|
+
if (!indicator || !keyDisplay) return;
|
|
656
|
+
|
|
657
|
+
if (!window.UplinkSatelliteSync) {
|
|
658
|
+
getLogger().warn('Satellites: UplinkSatelliteSync not loaded yet');
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Show loading state
|
|
663
|
+
indicator.className = 'session-indicator loading';
|
|
664
|
+
indicator.title = 'Checking session...';
|
|
665
|
+
keyDisplay.textContent = 'checking...';
|
|
666
|
+
|
|
667
|
+
const satAgentId = satellites[satId]?.agentId || 'main';
|
|
668
|
+
const data = await window.UplinkSatelliteSync.fetchSessionStatus(satId, satAgentId);
|
|
669
|
+
|
|
670
|
+
if (!data) {
|
|
671
|
+
indicator.className = 'session-indicator error';
|
|
672
|
+
indicator.title = 'Failed to check session';
|
|
673
|
+
keyDisplay.textContent = 'error';
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (data.gatewayConnected) {
|
|
678
|
+
indicator.className = 'session-indicator connected';
|
|
679
|
+
indicator.title = 'Connected to OpenClaw';
|
|
680
|
+
} else {
|
|
681
|
+
indicator.className = 'session-indicator disconnected';
|
|
682
|
+
indicator.title = 'Gateway offline';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Show abbreviated session key
|
|
686
|
+
const shortKey = data.sessionKey.replace('agent:main:', '');
|
|
687
|
+
keyDisplay.textContent = shortKey;
|
|
688
|
+
keyDisplay.title = `Session: ${data.sessionKey}\nClick to copy`;
|
|
689
|
+
|
|
690
|
+
// Click to copy
|
|
691
|
+
keyDisplay.onclick = () => {
|
|
692
|
+
navigator.clipboard.writeText(data.sessionKey);
|
|
693
|
+
showNotification('Session key copied');
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function deleteSatellite(satId, skipConfirm = false) {
|
|
698
|
+
console.log('[Satellites] deleteSatellite START, panelVisible:', panelVisible);
|
|
699
|
+
|
|
700
|
+
// Primary satellite cannot be deleted (button is disabled in UI as visual feedback)
|
|
701
|
+
if (satId === PRIMARY_SATELLITE) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const satellite = satellites[satId];
|
|
706
|
+
const isCurrentSatellite = satId === currentSatellite;
|
|
707
|
+
|
|
708
|
+
// Check for pending delete state (click delete twice to confirm)
|
|
709
|
+
if (!skipConfirm) {
|
|
710
|
+
if (!satellite._pendingDelete) {
|
|
711
|
+
// First click - set pending state and show visual feedback
|
|
712
|
+
satellite._pendingDelete = Date.now();
|
|
713
|
+
|
|
714
|
+
// Extra warning if retiring the current satellite
|
|
715
|
+
if (isCurrentSatellite) {
|
|
716
|
+
showNotification('⚠️ You\'re in this satellite! Click again to retire it.', 'warning');
|
|
717
|
+
} else {
|
|
718
|
+
showNotification('Are you sure you want to retire this satellite?', 'info');
|
|
719
|
+
}
|
|
720
|
+
console.log('[Satellites] After showNotification, panelVisible:', panelVisible);
|
|
721
|
+
updateNavigator(); // Re-render to show pending state
|
|
722
|
+
console.log('[Satellites] After updateNavigator, panelVisible:', panelVisible);
|
|
723
|
+
|
|
724
|
+
// Clear any existing timer for this satellite
|
|
725
|
+
if (pendingDeleteTimers.has(satId)) {
|
|
726
|
+
clearTimeout(pendingDeleteTimers.get(satId));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Auto-clear pending state after 5 seconds
|
|
730
|
+
const timerId = setTimeout(() => {
|
|
731
|
+
if (satellite._pendingDelete) {
|
|
732
|
+
delete satellite._pendingDelete;
|
|
733
|
+
updateNavigator();
|
|
734
|
+
}
|
|
735
|
+
pendingDeleteTimers.delete(satId);
|
|
736
|
+
}, 5000);
|
|
737
|
+
pendingDeleteTimers.set(satId, timerId);
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Second click on current satellite - show final warning with message count
|
|
742
|
+
if (isCurrentSatellite && !satellite._confirmedCurrentDelete) {
|
|
743
|
+
const msgCount = getMessageCountInSatellite(satId);
|
|
744
|
+
satellite._confirmedCurrentDelete = true;
|
|
745
|
+
showNotification(`⚠️ This will delete ${msgCount} messages. Click once more to confirm.`, 'warning');
|
|
746
|
+
|
|
747
|
+
// Reset the confirmation after 5 seconds
|
|
748
|
+
setTimeout(() => {
|
|
749
|
+
if (satellite._confirmedCurrentDelete) {
|
|
750
|
+
delete satellite._confirmedCurrentDelete;
|
|
751
|
+
delete satellite._pendingDelete;
|
|
752
|
+
updateNavigator();
|
|
753
|
+
}
|
|
754
|
+
}, 5000);
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Call the retire API to also delete the OpenClaw session
|
|
760
|
+
retireSatelliteSession(satId);
|
|
761
|
+
|
|
762
|
+
// If we're on this satellite, switch to primary
|
|
763
|
+
if (currentSatellite === satId) {
|
|
764
|
+
currentSatellite = PRIMARY_SATELLITE;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
delete satellites[satId];
|
|
768
|
+
saveSatellites();
|
|
769
|
+
reloadChatDisplay();
|
|
770
|
+
updateNavigator();
|
|
771
|
+
|
|
772
|
+
// Ensure panel stays visible after delete (fix for splitview state desync)
|
|
773
|
+
if (navigatorPanel) {
|
|
774
|
+
navigatorPanel.style.display = 'flex';
|
|
775
|
+
navigatorPanel.classList.add('visible');
|
|
776
|
+
}
|
|
777
|
+
// Also ensure splitview side panel stays open if on desktop
|
|
778
|
+
const sidePanel = document.getElementById('sidePanel');
|
|
779
|
+
if (sidePanel) {
|
|
780
|
+
sidePanel.classList.add('visible');
|
|
781
|
+
document.body.classList.add('panel-open');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
showNotification('Satellite decommissioned');
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Call the backend to retire a satellite's OpenClaw session
|
|
790
|
+
* This deletes both the session entry and transcript from OpenClaw
|
|
791
|
+
*/
|
|
792
|
+
async function retireSatelliteSession(satId) {
|
|
793
|
+
if (!window.UplinkSatelliteSync) {
|
|
794
|
+
getLogger().warn('Satellites: UplinkSatelliteSync not loaded yet');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const result = await window.UplinkSatelliteSync.retireSatelliteSession(
|
|
799
|
+
satId,
|
|
800
|
+
satellites[satId]?.agentId || 'main'
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
// Show additional feedback if OpenClaw session was deleted
|
|
804
|
+
if (result && (result.sessionDeleted || result.transcriptDeleted)) {
|
|
805
|
+
showNotification('Session data cleared from OpenClaw', 'info');
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function setAsPrimary(satId) {
|
|
810
|
+
if (!satellites[satId]) return;
|
|
811
|
+
|
|
812
|
+
// Just set this satellite as the default (pinned to top)
|
|
813
|
+
defaultSatellite = satId;
|
|
814
|
+
saveSatellites();
|
|
815
|
+
|
|
816
|
+
// Switch to it
|
|
817
|
+
connectToSatellite(satId);
|
|
818
|
+
updateNavigator();
|
|
819
|
+
|
|
820
|
+
showNotification(`"${satellites[satId].name}" is now default`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function renameSatellite(satId) {
|
|
824
|
+
const satellite = satellites[satId];
|
|
825
|
+
if (!satellite) return;
|
|
826
|
+
|
|
827
|
+
// Try native prompt first
|
|
828
|
+
let newName = prompt('Rename satellite:', satellite.name);
|
|
829
|
+
|
|
830
|
+
// If prompt returns null (cancelled or not working), use inline edit
|
|
831
|
+
if (newName === null) {
|
|
832
|
+
// Find the satellite item and make name editable
|
|
833
|
+
const item = navigatorPanel?.querySelector(`[data-satellite-id="${satId}"]`);
|
|
834
|
+
const nameEl = item?.querySelector('.satellite-item-name');
|
|
835
|
+
if (nameEl) {
|
|
836
|
+
const input = document.createElement('input');
|
|
837
|
+
input.type = 'text';
|
|
838
|
+
input.value = satellite.name;
|
|
839
|
+
input.className = 'satellite-rename-input';
|
|
840
|
+
input.style.cssText = 'width: 100px; padding: 2px 4px; font-size: inherit; border: 1px solid var(--accent); border-radius: 4px; background: var(--bg-secondary); color: inherit;';
|
|
841
|
+
|
|
842
|
+
const originalText = nameEl.textContent;
|
|
843
|
+
nameEl.textContent = '';
|
|
844
|
+
nameEl.appendChild(input);
|
|
845
|
+
input.focus();
|
|
846
|
+
input.select();
|
|
847
|
+
|
|
848
|
+
const finishEdit = (save) => {
|
|
849
|
+
const val = input.value.trim();
|
|
850
|
+
if (save && val) {
|
|
851
|
+
satellite.name = val;
|
|
852
|
+
saveSatellites();
|
|
853
|
+
}
|
|
854
|
+
nameEl.textContent = save && val ? val : originalText;
|
|
855
|
+
updateNavigator();
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
input.addEventListener('blur', () => finishEdit(true));
|
|
859
|
+
input.addEventListener('keydown', (e) => {
|
|
860
|
+
if (e.key === 'Enter') { e.preventDefault(); finishEdit(true); }
|
|
861
|
+
if (e.key === 'Escape') { e.preventDefault(); finishEdit(false); }
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (newName.trim() === '') return;
|
|
868
|
+
|
|
869
|
+
satellite.name = newName.trim();
|
|
870
|
+
saveSatellites();
|
|
871
|
+
updateNavigator();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ============================================
|
|
875
|
+
// MESSAGE HISTORY
|
|
876
|
+
// ============================================
|
|
877
|
+
|
|
878
|
+
function getMessageHistory(satId = currentSatellite) {
|
|
879
|
+
const satellite = satellites[satId];
|
|
880
|
+
if (!satellite) return [];
|
|
881
|
+
return satellite.messages || [];
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function addMessageToSatellite(msg) {
|
|
885
|
+
console.log('[Satellites] addMessageToSatellite called, currentSatellite:', currentSatellite, 'msg type:', msg.type);
|
|
886
|
+
|
|
887
|
+
// Main satellite uses Gateway as source of truth for text messages
|
|
888
|
+
// But we DO save image messages locally since Gateway doesn't track them
|
|
889
|
+
if (currentSatellite === 'main') {
|
|
890
|
+
if (!msg.imageUrl) {
|
|
891
|
+
console.log('[Satellites] Skipping local save for main satellite (Gateway is source of truth)');
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
console.log('[Satellites] Saving image message locally for main satellite');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!satellites[currentSatellite]) {
|
|
898
|
+
console.warn('[Satellites] currentSatellite not found in satellites object!');
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Don't store data URLs — they're too large for localStorage
|
|
903
|
+
const safeMsg = { ...msg };
|
|
904
|
+
if (safeMsg.imageUrl && safeMsg.imageUrl.startsWith('data:')) {
|
|
905
|
+
safeMsg.imageUrl = '__pending_upload__';
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
satellites[currentSatellite].messages.push({
|
|
909
|
+
...safeMsg,
|
|
910
|
+
timestamp: safeMsg.timestamp || Date.now()
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
console.log('[Satellites] Message saved. Total messages for', currentSatellite, ':', satellites[currentSatellite].messages.length);
|
|
914
|
+
saveSatellites();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function getMessageCountInSatellite(satId) {
|
|
918
|
+
return (satellites[satId]?.messages || []).length;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// ============================================
|
|
922
|
+
// UI: NAVIGATOR PANEL
|
|
923
|
+
// ============================================
|
|
924
|
+
|
|
925
|
+
// Active tab: 'satellites' or 'agents'
|
|
926
|
+
let activeTab = 'satellites';
|
|
927
|
+
|
|
928
|
+
function switchTab(tab) {
|
|
929
|
+
// Premium gate: block agents tab for free users
|
|
930
|
+
if (tab === 'agents' && window.UplinkPremium && !window.UplinkPremium.isActive()) {
|
|
931
|
+
window.UplinkPremium.showUpgradeModal('Agent management');
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
activeTab = tab;
|
|
936
|
+
const satTabs = navigatorPanel.querySelectorAll('[data-nav-tab="satellites"]');
|
|
937
|
+
const agentsTabs = navigatorPanel.querySelectorAll('[data-nav-tab="agents"]');
|
|
938
|
+
const satContent = navigatorPanel.querySelector('.satellite-tab-content');
|
|
939
|
+
const agentsContent = navigatorPanel.querySelector('.agents-tab-content');
|
|
940
|
+
|
|
941
|
+
if (tab === 'satellites') {
|
|
942
|
+
satTabs.forEach(t => t.classList.add('active'));
|
|
943
|
+
agentsTabs.forEach(t => t.classList.remove('active'));
|
|
944
|
+
if (satContent) satContent.style.display = '';
|
|
945
|
+
if (agentsContent) agentsContent.style.display = 'none';
|
|
946
|
+
} else {
|
|
947
|
+
satTabs.forEach(t => t.classList.remove('active'));
|
|
948
|
+
agentsTabs.forEach(t => t.classList.add('active'));
|
|
949
|
+
if (satContent) satContent.style.display = 'none';
|
|
950
|
+
if (agentsContent) agentsContent.style.display = '';
|
|
951
|
+
// Render agents into the content area
|
|
952
|
+
if (window.UplinkAgents && agentsContent) {
|
|
953
|
+
window.UplinkAgents.render(agentsContent);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function createNavigatorPanel() {
|
|
959
|
+
navigatorPanel = document.createElement('div');
|
|
960
|
+
navigatorPanel.className = 'satellite-navigator';
|
|
961
|
+
navigatorPanel.innerHTML = `
|
|
962
|
+
<div class="satellite-nav-header">
|
|
963
|
+
<div class="satellite-nav-tabs" role="tablist">
|
|
964
|
+
<button class="satellite-nav-tab active" data-nav-tab="satellites" role="tab" aria-selected="true">Sessions</button>
|
|
965
|
+
<button class="satellite-nav-tab" data-nav-tab="agents" role="tab" aria-selected="false">Agents</button>
|
|
966
|
+
</div>
|
|
967
|
+
<button class="satellite-nav-close" aria-label="Close panel"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="1" y1="1" x2="13" y2="13"/><line x1="13" y1="1" x2="1" y2="13"/></svg></button>
|
|
968
|
+
</div>
|
|
969
|
+
<div class="satellite-nav-tabs-inline" role="tablist" style="display: none;">
|
|
970
|
+
<button class="satellite-nav-tab active" data-nav-tab="satellites" role="tab" aria-selected="true">Sessions</button>
|
|
971
|
+
<button class="satellite-nav-tab" data-nav-tab="agents" role="tab" aria-selected="false">Agents</button>
|
|
972
|
+
</div>
|
|
973
|
+
<div class="satellite-tab-content">
|
|
974
|
+
<div class="satellite-session-status" id="sessionStatus">
|
|
975
|
+
<span class="session-indicator" id="sessionIndicator">●</span>
|
|
976
|
+
<span class="session-key" id="sessionKeyDisplay" title="Click to copy"></span>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="satellite-list" id="satelliteList" role="listbox" aria-label="Satellite list"></div>
|
|
979
|
+
<div class="satellite-nav-actions">
|
|
980
|
+
<button class="satellite-nav-btn" id="launchSatelliteBtn">+ Launch New</button>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
<div class="agents-tab-content" style="display: none;"></div>
|
|
984
|
+
`;
|
|
985
|
+
|
|
986
|
+
navigatorPanel.style.display = 'none';
|
|
987
|
+
document.body.appendChild(navigatorPanel);
|
|
988
|
+
|
|
989
|
+
// Tab switching
|
|
990
|
+
navigatorPanel.querySelectorAll('.satellite-nav-tab').forEach(tab => {
|
|
991
|
+
tab.addEventListener('click', () => switchTab(tab.dataset.navTab));
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// Event listeners
|
|
995
|
+
navigatorPanel.querySelector('.satellite-nav-close').addEventListener('click', function() {
|
|
996
|
+
// If split view is active (desktop), close via split view
|
|
997
|
+
if (window.UplinkSplitView && window.UplinkSplitView.isDesktop() && window.UplinkSplitView.getCurrentPanel() === 'satellites') {
|
|
998
|
+
window.UplinkSplitView.closePanel();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
var panels = getPanels();
|
|
1002
|
+
if (panels) {
|
|
1003
|
+
panels.close('satellites');
|
|
1004
|
+
} else {
|
|
1005
|
+
hideNavigator();
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
navigatorPanel.querySelector('#launchSatelliteBtn').addEventListener('click', () => {
|
|
1009
|
+
promptForSatelliteName();
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// Bind to existing toggle button in header (or create one as fallback)
|
|
1013
|
+
let toggleBtn = document.getElementById('satellitesBtn');
|
|
1014
|
+
if (!toggleBtn) {
|
|
1015
|
+
const header = document.querySelector('.header-right, .header-actions');
|
|
1016
|
+
if (header) {
|
|
1017
|
+
toggleBtn = document.createElement('button');
|
|
1018
|
+
toggleBtn.id = 'satellitesBtn';
|
|
1019
|
+
toggleBtn.className = 'header-btn satellite-toggle';
|
|
1020
|
+
toggleBtn.innerHTML = '<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="8" stroke-dasharray="2 4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2"/></svg>';
|
|
1021
|
+
toggleBtn.title = 'Satellites';
|
|
1022
|
+
toggleBtn.setAttribute('aria-label', 'Satellites');
|
|
1023
|
+
header.insertBefore(toggleBtn, header.firstChild);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// Bind click handler (splitview intercepts on desktop, this handles mobile)
|
|
1027
|
+
if (toggleBtn) {
|
|
1028
|
+
toggleBtn.addEventListener('click', toggleNavigator);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Register with panel manager for mutual exclusivity
|
|
1032
|
+
registerWithPanelManager();
|
|
1033
|
+
|
|
1034
|
+
updateNavigator();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function updateNavigator() {
|
|
1038
|
+
const listEl = document.getElementById('satelliteList');
|
|
1039
|
+
const currentNameEl = document.getElementById('currentSatelliteName');
|
|
1040
|
+
|
|
1041
|
+
if (!listEl) return;
|
|
1042
|
+
|
|
1043
|
+
// Update current satellite name
|
|
1044
|
+
if (currentNameEl && satellites[currentSatellite]) {
|
|
1045
|
+
currentNameEl.textContent = satellites[currentSatellite].name;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Build list view
|
|
1049
|
+
const listHtml = buildSatelliteList();
|
|
1050
|
+
listEl.innerHTML = listHtml;
|
|
1051
|
+
|
|
1052
|
+
// Add event listeners to satellite items
|
|
1053
|
+
const items = listEl.querySelectorAll('.satellite-item');
|
|
1054
|
+
items.forEach(item => {
|
|
1055
|
+
const satId = item.dataset.satelliteId;
|
|
1056
|
+
|
|
1057
|
+
// Click anywhere on the item to connect (except action buttons)
|
|
1058
|
+
item.addEventListener('click', (e) => {
|
|
1059
|
+
if (!e.target.closest('.satellite-item-actions')) {
|
|
1060
|
+
connectToSatellite(satId);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// Keyboard navigation
|
|
1065
|
+
item.addEventListener('keydown', (e) => {
|
|
1066
|
+
const allItems = Array.from(items);
|
|
1067
|
+
const currentIndex = allItems.indexOf(item);
|
|
1068
|
+
|
|
1069
|
+
switch (e.key) {
|
|
1070
|
+
case 'ArrowDown':
|
|
1071
|
+
e.preventDefault();
|
|
1072
|
+
if (currentIndex < allItems.length - 1) {
|
|
1073
|
+
allItems[currentIndex + 1].focus();
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
case 'ArrowUp':
|
|
1077
|
+
e.preventDefault();
|
|
1078
|
+
if (currentIndex > 0) {
|
|
1079
|
+
allItems[currentIndex - 1].focus();
|
|
1080
|
+
}
|
|
1081
|
+
break;
|
|
1082
|
+
case 'Enter':
|
|
1083
|
+
case ' ':
|
|
1084
|
+
e.preventDefault();
|
|
1085
|
+
connectToSatellite(satId);
|
|
1086
|
+
break;
|
|
1087
|
+
case 'Delete':
|
|
1088
|
+
case 'Backspace':
|
|
1089
|
+
e.preventDefault();
|
|
1090
|
+
if (satId !== PRIMARY_SATELLITE) {
|
|
1091
|
+
deleteSatellite(satId);
|
|
1092
|
+
}
|
|
1093
|
+
break;
|
|
1094
|
+
case 'F2':
|
|
1095
|
+
e.preventDefault();
|
|
1096
|
+
renameSatellite(satId);
|
|
1097
|
+
break;
|
|
1098
|
+
case 'Home':
|
|
1099
|
+
e.preventDefault();
|
|
1100
|
+
allItems[0]?.focus();
|
|
1101
|
+
break;
|
|
1102
|
+
case 'End':
|
|
1103
|
+
e.preventDefault();
|
|
1104
|
+
allItems[allItems.length - 1]?.focus();
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
item.querySelector('.satellite-item-rename')?.addEventListener('click', (e) => {
|
|
1110
|
+
e.stopPropagation();
|
|
1111
|
+
renameSatellite(satId);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
item.querySelector('.satellite-item-delete')?.addEventListener('click', (e) => {
|
|
1115
|
+
console.log('[Satellites] Delete button clicked for:', satId);
|
|
1116
|
+
e.stopPropagation();
|
|
1117
|
+
e.preventDefault();
|
|
1118
|
+
const satellite = satellites[satId];
|
|
1119
|
+
const skipConfirm = satellite?._pendingDelete && (Date.now() - satellite._pendingDelete < 3000);
|
|
1120
|
+
console.log('[Satellites] skipConfirm:', skipConfirm, 'pendingDelete:', satellite?._pendingDelete);
|
|
1121
|
+
if (skipConfirm && satellite) delete satellite._pendingDelete;
|
|
1122
|
+
deleteSatellite(satId, skipConfirm);
|
|
1123
|
+
console.log('[Satellites] After deleteSatellite, panelVisible:', panelVisible);
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function buildSatelliteList() {
|
|
1129
|
+
// Sort satellites: primary first, then by most recently active
|
|
1130
|
+
const sortedIds = Object.keys(satellites).sort((a, b) => {
|
|
1131
|
+
if (a === PRIMARY_SATELLITE) return -1;
|
|
1132
|
+
if (b === PRIMARY_SATELLITE) return 1;
|
|
1133
|
+
return (satellites[b].createdAt || 0) - (satellites[a].createdAt || 0);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
return sortedIds.map(id => {
|
|
1137
|
+
const satellite = satellites[id];
|
|
1138
|
+
const isCurrent = id === currentSatellite;
|
|
1139
|
+
const isPrimary = id === PRIMARY_SATELLITE;
|
|
1140
|
+
const isDefault = id === defaultSatellite;
|
|
1141
|
+
const msgCount = getMessageCountInSatellite(id);
|
|
1142
|
+
const isPendingDelete = !!satellite._pendingDelete;
|
|
1143
|
+
|
|
1144
|
+
const classes = ['panel-item', 'satellite-item'];
|
|
1145
|
+
if (isCurrent) classes.push('active', 'current');
|
|
1146
|
+
if (isPendingDelete) classes.push('pending-delete');
|
|
1147
|
+
|
|
1148
|
+
// Icon: globe for primary, satellite for everything else
|
|
1149
|
+
const icon = isPrimary ? '🌍' : '🛰️';
|
|
1150
|
+
|
|
1151
|
+
return `
|
|
1152
|
+
<div class="${classes.join(' ')}"
|
|
1153
|
+
data-satellite-id="${id}"
|
|
1154
|
+
role="option"
|
|
1155
|
+
aria-selected="${isCurrent}"
|
|
1156
|
+
tabindex="${isCurrent ? '0' : '-1'}">
|
|
1157
|
+
<span class="panel-item-icon satellite-item-icon" aria-hidden="true">${icon}</span>
|
|
1158
|
+
<span class="panel-item-name satellite-item-name">${escapeHtml(satellite.name)}</span>
|
|
1159
|
+
<div class="satellite-item-actions">
|
|
1160
|
+
<button class="satellite-item-btn satellite-item-rename" title="Rename" aria-label="Rename ${escapeHtml(satellite.name)}">✏️</button>
|
|
1161
|
+
${!isPrimary ? `
|
|
1162
|
+
<button class="satellite-item-btn satellite-item-delete" title="${isPendingDelete ? 'Confirm retire' : 'Retire'}" aria-label="${isPendingDelete ? 'Confirm retire' : 'Retire'} ${escapeHtml(satellite.name)}">${isPendingDelete ? '⚠️' : '🗑️'}</button>
|
|
1163
|
+
` : ''}
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
`;
|
|
1167
|
+
}).join('');
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function showNavigator() {
|
|
1171
|
+
if (navigatorPanel) {
|
|
1172
|
+
navigatorPanel.style.display = 'flex';
|
|
1173
|
+
navigatorPanel.classList.add('visible');
|
|
1174
|
+
panelVisible = true;
|
|
1175
|
+
updateNavigator();
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function hideNavigator() {
|
|
1180
|
+
console.log('[Satellites] hideNavigator called');
|
|
1181
|
+
console.trace('[Satellites] hideNavigator stack trace');
|
|
1182
|
+
if (navigatorPanel) {
|
|
1183
|
+
navigatorPanel.classList.remove('visible');
|
|
1184
|
+
// Delay hiding display to allow transition to complete
|
|
1185
|
+
setTimeout(() => {
|
|
1186
|
+
if (!panelVisible) {
|
|
1187
|
+
navigatorPanel.style.display = 'none';
|
|
1188
|
+
}
|
|
1189
|
+
}, 300);
|
|
1190
|
+
panelVisible = false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function toggleNavigator() {
|
|
1195
|
+
console.log('[Satellites] toggleNavigator called');
|
|
1196
|
+
console.trace('[Satellites] toggleNavigator stack trace');
|
|
1197
|
+
const panels = getPanels();
|
|
1198
|
+
if (panels) {
|
|
1199
|
+
panelVisible = panels.toggle('satellites');
|
|
1200
|
+
console.log('[Satellites] panels.toggle returned:', panelVisible);
|
|
1201
|
+
if (panelVisible) updateNavigator();
|
|
1202
|
+
} else {
|
|
1203
|
+
if (panelVisible) {
|
|
1204
|
+
hideNavigator();
|
|
1205
|
+
} else {
|
|
1206
|
+
showNavigator();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function registerWithPanelManager() {
|
|
1212
|
+
const panels = getPanels();
|
|
1213
|
+
if (panels && navigatorPanel) {
|
|
1214
|
+
panels.register('satellites', {
|
|
1215
|
+
element: navigatorPanel,
|
|
1216
|
+
isOpen: () => panelVisible,
|
|
1217
|
+
open: showNavigator,
|
|
1218
|
+
close: hideNavigator
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// ============================================
|
|
1224
|
+
// CHAT INTEGRATION
|
|
1225
|
+
// ============================================
|
|
1226
|
+
|
|
1227
|
+
function reloadChatDisplay() {
|
|
1228
|
+
console.log('[Satellites] reloadChatDisplay called');
|
|
1229
|
+
const messagesEl = document.getElementById('messages');
|
|
1230
|
+
const emptyStateEl = document.querySelector('.empty-state');
|
|
1231
|
+
|
|
1232
|
+
if (!messagesEl) {
|
|
1233
|
+
console.warn('[Satellites] messagesEl not found, retrying...');
|
|
1234
|
+
setTimeout(reloadChatDisplay, 100);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// DON'T wipe if streaming is in progress - the streaming div would be destroyed
|
|
1239
|
+
const streamingDiv = messagesEl.querySelector('.message.streaming');
|
|
1240
|
+
const isProcessing = window.UplinkCore?.chatState === 'processing';
|
|
1241
|
+
if (streamingDiv || isProcessing) {
|
|
1242
|
+
return; // Don't wipe DOM during streaming
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Clear current messages
|
|
1246
|
+
messagesEl.innerHTML = '';
|
|
1247
|
+
|
|
1248
|
+
// For main satellite, messages come from Gateway (set by fetchAndMergeGatewayHistory)
|
|
1249
|
+
// For other satellites, use local storage
|
|
1250
|
+
const messages = currentSatellite === 'main'
|
|
1251
|
+
? (satellites[currentSatellite]?.messages || []) // Gateway messages merged here
|
|
1252
|
+
: getMessageHistory();
|
|
1253
|
+
|
|
1254
|
+
console.log('[Satellites] Reloading display with', messages.length, 'messages for', currentSatellite);
|
|
1255
|
+
|
|
1256
|
+
if (messages.length === 0) {
|
|
1257
|
+
if (emptyStateEl) emptyStateEl.style.display = 'flex';
|
|
1258
|
+
} else {
|
|
1259
|
+
if (emptyStateEl) emptyStateEl.style.display = 'none';
|
|
1260
|
+
|
|
1261
|
+
// Try to use the message API first
|
|
1262
|
+
const msgApi = getMessageApi();
|
|
1263
|
+
console.log('[Satellites] Message API:', msgApi ? 'available' : 'NOT available');
|
|
1264
|
+
|
|
1265
|
+
messages.forEach(msg => {
|
|
1266
|
+
if (msgApi && msgApi.addMessage) {
|
|
1267
|
+
msgApi.addMessage(msg.text, msg.type, msg.imageUrl, false, msg.timestamp || null);
|
|
1268
|
+
} else {
|
|
1269
|
+
// Fallback: render directly to DOM if message API unavailable
|
|
1270
|
+
renderMessageDirectly(messagesEl, msg);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// Scroll to bottom after loading
|
|
1275
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Fallback renderer when message API is unavailable
|
|
1281
|
+
*/
|
|
1282
|
+
function renderMessageDirectly(container, msg) {
|
|
1283
|
+
const div = document.createElement('div');
|
|
1284
|
+
div.className = `message ${msg.type}`;
|
|
1285
|
+
|
|
1286
|
+
if (msg.imageUrl) {
|
|
1287
|
+
const img = document.createElement('img');
|
|
1288
|
+
img.src = msg.imageUrl;
|
|
1289
|
+
img.alt = msg.type === 'user' ? 'Image shared by you' : 'Image from assistant';
|
|
1290
|
+
div.appendChild(img);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (msg.text) {
|
|
1294
|
+
const textSpan = document.createElement('span');
|
|
1295
|
+
textSpan.className = 'message-text';
|
|
1296
|
+
// Basic formatting - the full formatMessage is in chat.js
|
|
1297
|
+
textSpan.innerHTML = msg.text
|
|
1298
|
+
.replace(/&/g, '&')
|
|
1299
|
+
.replace(/</g, '<')
|
|
1300
|
+
.replace(/>/g, '>')
|
|
1301
|
+
.replace(/\n/g, '<br>');
|
|
1302
|
+
div.appendChild(textSpan);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
container.appendChild(div);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function injectMessageHooks(retryCount = 0) {
|
|
1309
|
+
// Prevent multiple registration
|
|
1310
|
+
if (messageHookInstalled) return;
|
|
1311
|
+
|
|
1312
|
+
console.log('[Satellites] injectMessageHooks attempt', retryCount, '- UplinkChat:', !!window.UplinkChat, 'onMessage:', !!(window.UplinkChat && window.UplinkChat.onMessage));
|
|
1313
|
+
|
|
1314
|
+
// If we have an injected messageApi, use that
|
|
1315
|
+
if (deps.messageApi) {
|
|
1316
|
+
messageHookInstalled = true;
|
|
1317
|
+
console.log('[Satellites] Using injected message API');
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Use the proper UplinkChat.onMessage hook API
|
|
1322
|
+
if (window.UplinkChat && window.UplinkChat.onMessage) {
|
|
1323
|
+
// Register with the chat module's hook system
|
|
1324
|
+
window.UplinkChat.onMessage(({ text, type, imageUrl, save }) => {
|
|
1325
|
+
// Save to current satellite if this is a new message (not replay)
|
|
1326
|
+
if (save && type !== 'system') {
|
|
1327
|
+
addMessageToSatellite({ text, type, imageUrl });
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
// Store reference to addMessage for reloadChatDisplay
|
|
1332
|
+
window._originalAddMessage = window.UplinkChat.addMessage || window.addMessage;
|
|
1333
|
+
|
|
1334
|
+
messageHookInstalled = true;
|
|
1335
|
+
console.log('[Satellites] Message hook registered via UplinkChat.onMessage');
|
|
1336
|
+
} else if (retryCount < 20) {
|
|
1337
|
+
// Retry with exponential backoff
|
|
1338
|
+
const delay = Math.min(100 * Math.pow(1.5, retryCount), 1000);
|
|
1339
|
+
setTimeout(() => injectMessageHooks(retryCount + 1), delay);
|
|
1340
|
+
} else {
|
|
1341
|
+
console.warn('[Satellites] Could not register message hook after 20 retries - falling back to event listener');
|
|
1342
|
+
// Fallback: listen for chat module ready event
|
|
1343
|
+
window.addEventListener('uplink:ready', () => {
|
|
1344
|
+
console.log('[Satellites] uplink:ready event received, retrying hook installation');
|
|
1345
|
+
if (window.UplinkChat && !messageHookInstalled) {
|
|
1346
|
+
injectMessageHooks(0);
|
|
1347
|
+
}
|
|
1348
|
+
}, { once: true });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// ============================================
|
|
1353
|
+
// NOTIFICATIONS
|
|
1354
|
+
// ============================================
|
|
1355
|
+
|
|
1356
|
+
function showNotification(message, type = 'success') {
|
|
1357
|
+
// Try to use injected or global notification system
|
|
1358
|
+
const notifications = getNotifications();
|
|
1359
|
+
if (notifications?.show) {
|
|
1360
|
+
notifications.show(message, type);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Fallback: create simple toast
|
|
1365
|
+
const toast = document.createElement('div');
|
|
1366
|
+
toast.className = `satellite-toast satellite-toast-${type}`;
|
|
1367
|
+
toast.textContent = message;
|
|
1368
|
+
// M-35: Announce satellite notifications to screen readers
|
|
1369
|
+
toast.setAttribute('role', 'alert');
|
|
1370
|
+
toast.setAttribute('aria-live', 'polite');
|
|
1371
|
+
toast.style.cssText = `
|
|
1372
|
+
position: fixed;
|
|
1373
|
+
bottom: 80px;
|
|
1374
|
+
left: 50%;
|
|
1375
|
+
transform: translateX(-50%);
|
|
1376
|
+
background: var(--bg-secondary, #333);
|
|
1377
|
+
color: var(--text-primary, #fff);
|
|
1378
|
+
padding: 12px 24px;
|
|
1379
|
+
border-radius: 8px;
|
|
1380
|
+
z-index: 10000;
|
|
1381
|
+
animation: fadeInUp 0.3s ease;
|
|
1382
|
+
`;
|
|
1383
|
+
document.body.appendChild(toast);
|
|
1384
|
+
|
|
1385
|
+
setTimeout(() => {
|
|
1386
|
+
toast.style.animation = 'fadeOutDown 0.3s ease';
|
|
1387
|
+
setTimeout(() => toast.remove(), 300);
|
|
1388
|
+
}, 2000);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// ============================================
|
|
1392
|
+
// UTILITIES
|
|
1393
|
+
// ============================================
|
|
1394
|
+
|
|
1395
|
+
function escapeHtml(text) {
|
|
1396
|
+
const div = document.createElement('div');
|
|
1397
|
+
div.textContent = text;
|
|
1398
|
+
return div.innerHTML;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// ============================================
|
|
1402
|
+
// MIGRATION
|
|
1403
|
+
// ============================================
|
|
1404
|
+
|
|
1405
|
+
async function migrateExistingHistory() {
|
|
1406
|
+
// Only migrate if primary satellite is empty and there's existing history
|
|
1407
|
+
if (satellites[PRIMARY_SATELLITE].messages.length > 0) return;
|
|
1408
|
+
|
|
1409
|
+
// Check for old branches data
|
|
1410
|
+
try {
|
|
1411
|
+
const oldBranches = localStorage.getItem('uplink-branches');
|
|
1412
|
+
if (oldBranches) {
|
|
1413
|
+
const data = JSON.parse(oldBranches);
|
|
1414
|
+
if (data.branches?.main?.messages?.length > 0) {
|
|
1415
|
+
satellites[PRIMARY_SATELLITE].messages = data.branches.main.messages;
|
|
1416
|
+
saveSatellites();
|
|
1417
|
+
getLogger().debug('Satellites: Migrated', satellites[PRIMARY_SATELLITE].messages.length, 'messages from branches');
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
} catch (e) {
|
|
1422
|
+
getLogger().debug('Satellites: No branch data to migrate');
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Try to migrate from storage module
|
|
1426
|
+
try {
|
|
1427
|
+
const storage = getStorage();
|
|
1428
|
+
const existingHistory = await storage?.loadHistory();
|
|
1429
|
+
if (existingHistory && existingHistory.length > 0) {
|
|
1430
|
+
satellites[PRIMARY_SATELLITE].messages = existingHistory;
|
|
1431
|
+
saveSatellites();
|
|
1432
|
+
getLogger().debug('Satellites: Migrated', existingHistory.length, 'messages to primary satellite');
|
|
1433
|
+
}
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
getLogger().error('Satellites: Migration failed', e);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// ============================================
|
|
1440
|
+
// PUBLIC API
|
|
1441
|
+
// ============================================
|
|
1442
|
+
|
|
1443
|
+
export const UplinkSatellites = {
|
|
1444
|
+
// Initialization with dependency injection
|
|
1445
|
+
init,
|
|
1446
|
+
|
|
1447
|
+
// Satellite operations
|
|
1448
|
+
launchSatellite,
|
|
1449
|
+
connectToSatellite,
|
|
1450
|
+
deleteSatellite,
|
|
1451
|
+
renameSatellite,
|
|
1452
|
+
|
|
1453
|
+
// Getters
|
|
1454
|
+
getCurrentSatellite: () => currentSatellite,
|
|
1455
|
+
getCurrentId: () => currentSatellite,
|
|
1456
|
+
getCurrentAgentId: () => (satellites[currentSatellite]?.agentId || 'main'),
|
|
1457
|
+
getSatellites: () => ({ ...satellites }),
|
|
1458
|
+
getMessageHistory,
|
|
1459
|
+
|
|
1460
|
+
// UI
|
|
1461
|
+
showNavigator,
|
|
1462
|
+
hideNavigator,
|
|
1463
|
+
toggleNavigator,
|
|
1464
|
+
|
|
1465
|
+
// History refresh (fetches from Gateway for main satellite)
|
|
1466
|
+
refreshHistory: loadHistoryOnConnect,
|
|
1467
|
+
|
|
1468
|
+
// Update image URL in stored messages (data URL → server URL)
|
|
1469
|
+
updateLastImageUrl: function(serverUrl) {
|
|
1470
|
+
const sat = satellites[currentSatellite];
|
|
1471
|
+
if (!sat || !sat.messages) return;
|
|
1472
|
+
|
|
1473
|
+
for (let i = sat.messages.length - 1; i >= 0; i--) {
|
|
1474
|
+
const url = sat.messages[i].imageUrl;
|
|
1475
|
+
if (url && (url === '__pending_upload__' || url.startsWith('data:'))) {
|
|
1476
|
+
sat.messages[i].imageUrl = serverUrl;
|
|
1477
|
+
saveSatellites();
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
import { UplinkCore } from './core.js';
|
|
1485
|
+
|
|
1486
|
+
// Backward compat: assign to window
|
|
1487
|
+
window.UplinkSatellites = UplinkSatellites;
|
|
1488
|
+
|
|
1489
|
+
// Helper for init retry with exponential backoff
|
|
1490
|
+
function scheduleInitRetry() {
|
|
1491
|
+
if (initRetryCount >= MAX_INIT_RETRIES) {
|
|
1492
|
+
getLogger().warn('Satellites: Max init retries reached, giving up');
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
initRetryCount++;
|
|
1496
|
+
const delay = Math.min(100 * Math.pow(2, initRetryCount), 5000);
|
|
1497
|
+
setTimeout(() => init(), delay);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// Cleanup function for timers
|
|
1501
|
+
function cleanup() {
|
|
1502
|
+
// Clear connection poll interval
|
|
1503
|
+
if (connectionPollInterval) {
|
|
1504
|
+
clearInterval(connectionPollInterval);
|
|
1505
|
+
connectionPollInterval = null;
|
|
1506
|
+
}
|
|
1507
|
+
// Clear all pending delete timers
|
|
1508
|
+
pendingDeleteTimers.forEach((timerId) => clearTimeout(timerId));
|
|
1509
|
+
pendingDeleteTimers.clear();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Cleanup on page unload
|
|
1513
|
+
window.addEventListener('beforeunload', cleanup);
|
|
1514
|
+
|
|
1515
|
+
// Register with core for coordinated initialization
|
|
1516
|
+
UplinkCore.registerModule('satellites', init);
|