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