@mooncompany/uplink-chat 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @mooncompany/uplink-chat might be problematic. Click here for more details.

Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/bin/uplink.js +279 -0
  4. package/middleware/error-handler.js +69 -0
  5. package/package.json +93 -0
  6. package/public/css/agents.36b98c0f.css +1469 -0
  7. package/public/css/agents.css +1469 -0
  8. package/public/css/app.a6a7f8f5.css +2731 -0
  9. package/public/css/app.css +2731 -0
  10. package/public/css/artifacts.css +444 -0
  11. package/public/css/commands.css +55 -0
  12. package/public/css/connection.css +131 -0
  13. package/public/css/dashboard.css +233 -0
  14. package/public/css/developer.css +328 -0
  15. package/public/css/files.css +123 -0
  16. package/public/css/markdown.css +156 -0
  17. package/public/css/message-actions.css +278 -0
  18. package/public/css/mobile.css +614 -0
  19. package/public/css/panels-unified.css +483 -0
  20. package/public/css/premium.css +415 -0
  21. package/public/css/realtime.css +189 -0
  22. package/public/css/satellites.css +401 -0
  23. package/public/css/shortcuts.css +185 -0
  24. package/public/css/split-view.4def0262.css +673 -0
  25. package/public/css/split-view.css +673 -0
  26. package/public/css/theme-generator.css +391 -0
  27. package/public/css/themes.css +387 -0
  28. package/public/css/timestamps.css +54 -0
  29. package/public/css/variables.css +78 -0
  30. package/public/dist/bundle.b55050c4.js +15757 -0
  31. package/public/favicon.svg +24 -0
  32. package/public/img/agents/ada.png +0 -0
  33. package/public/img/agents/clarice.png +0 -0
  34. package/public/img/agents/dennis-nedry.png +0 -0
  35. package/public/img/agents/elliot-alderson.png +0 -0
  36. package/public/img/agents/main.png +0 -0
  37. package/public/img/agents/scotty.png +0 -0
  38. package/public/img/agents/top-flight-security.png +0 -0
  39. package/public/index.html +1083 -0
  40. package/public/js/agents-data.js +234 -0
  41. package/public/js/agents-ui.js +72 -0
  42. package/public/js/agents.js +1525 -0
  43. package/public/js/app.js +79 -0
  44. package/public/js/appearance-settings.js +111 -0
  45. package/public/js/artifacts.js +432 -0
  46. package/public/js/audio-queue.js +168 -0
  47. package/public/js/bootstrap.js +54 -0
  48. package/public/js/chat.js +1211 -0
  49. package/public/js/commands.js +581 -0
  50. package/public/js/connection-api.js +121 -0
  51. package/public/js/connection.js +1231 -0
  52. package/public/js/context-tracker.js +271 -0
  53. package/public/js/core.js +172 -0
  54. package/public/js/dashboard.js +452 -0
  55. package/public/js/developer.js +432 -0
  56. package/public/js/encryption.js +124 -0
  57. package/public/js/errors.js +122 -0
  58. package/public/js/event-bus.js +77 -0
  59. package/public/js/fetch-utils.js +171 -0
  60. package/public/js/file-handler.js +229 -0
  61. package/public/js/files.js +352 -0
  62. package/public/js/gateway-chat.js +538 -0
  63. package/public/js/logger.js +112 -0
  64. package/public/js/markdown.js +190 -0
  65. package/public/js/message-actions.js +431 -0
  66. package/public/js/message-renderer.js +288 -0
  67. package/public/js/missed-messages.js +235 -0
  68. package/public/js/mobile-debug.js +95 -0
  69. package/public/js/notifications.js +367 -0
  70. package/public/js/offline-queue.js +178 -0
  71. package/public/js/onboarding.js +543 -0
  72. package/public/js/panels.js +156 -0
  73. package/public/js/premium.js +412 -0
  74. package/public/js/realtime-voice.js +844 -0
  75. package/public/js/satellite-sync.js +256 -0
  76. package/public/js/satellite-ui.js +175 -0
  77. package/public/js/satellites.js +1516 -0
  78. package/public/js/settings.js +1087 -0
  79. package/public/js/shortcuts.js +381 -0
  80. package/public/js/split-chat.js +1234 -0
  81. package/public/js/split-resize.js +211 -0
  82. package/public/js/splitview.js +340 -0
  83. package/public/js/storage.js +408 -0
  84. package/public/js/streaming-handler.js +324 -0
  85. package/public/js/stt-settings.js +316 -0
  86. package/public/js/theme-generator.js +661 -0
  87. package/public/js/themes.js +164 -0
  88. package/public/js/timestamps.js +198 -0
  89. package/public/js/tts-settings.js +575 -0
  90. package/public/js/ui.js +267 -0
  91. package/public/js/update-notifier.js +143 -0
  92. package/public/js/utils/constants.js +165 -0
  93. package/public/js/utils/sanitize.js +93 -0
  94. package/public/js/utils/sse-parser.js +195 -0
  95. package/public/js/voice.js +883 -0
  96. package/public/manifest.json +58 -0
  97. package/public/moon_texture.jpg +0 -0
  98. package/public/sw.js +221 -0
  99. package/public/three.min.js +6 -0
  100. package/server/channel.js +529 -0
  101. package/server/chat.js +270 -0
  102. package/server/config-store.js +362 -0
  103. package/server/config.js +159 -0
  104. package/server/context.js +131 -0
  105. package/server/gateway-commands.js +211 -0
  106. package/server/gateway-proxy.js +318 -0
  107. package/server/index.js +22 -0
  108. package/server/logger.js +89 -0
  109. package/server/middleware/auth.js +188 -0
  110. package/server/middleware.js +218 -0
  111. package/server/openclaw-discover.js +308 -0
  112. package/server/premium/index.js +156 -0
  113. package/server/premium/license.js +140 -0
  114. package/server/realtime/bridge.js +837 -0
  115. package/server/realtime/index.js +349 -0
  116. package/server/realtime/tts-stream.js +446 -0
  117. package/server/routes/agents.js +564 -0
  118. package/server/routes/artifacts.js +174 -0
  119. package/server/routes/chat.js +311 -0
  120. package/server/routes/config-settings.js +345 -0
  121. package/server/routes/config.js +603 -0
  122. package/server/routes/files.js +307 -0
  123. package/server/routes/index.js +18 -0
  124. package/server/routes/media.js +451 -0
  125. package/server/routes/missed-messages.js +107 -0
  126. package/server/routes/premium.js +75 -0
  127. package/server/routes/push.js +156 -0
  128. package/server/routes/satellite.js +406 -0
  129. package/server/routes/status.js +251 -0
  130. package/server/routes/stt.js +35 -0
  131. package/server/routes/voice.js +260 -0
  132. package/server/routes/webhooks.js +203 -0
  133. package/server/routes.js +206 -0
  134. package/server/runtime-config.js +336 -0
  135. package/server/share.js +305 -0
  136. package/server/stt/faster-whisper.js +72 -0
  137. package/server/stt/groq.js +51 -0
  138. package/server/stt/index.js +196 -0
  139. package/server/stt/openai.js +49 -0
  140. package/server/sync.js +244 -0
  141. package/server/tailscale-https.js +175 -0
  142. package/server/tts.js +646 -0
  143. package/server/update-checker.js +172 -0
  144. package/server/utils/filename.js +129 -0
  145. package/server/utils.js +147 -0
  146. package/server/watchdog.js +318 -0
  147. package/server/websocket/broadcast.js +359 -0
  148. package/server/websocket/connections.js +339 -0
  149. package/server/websocket/index.js +215 -0
  150. package/server/websocket/routing.js +277 -0
  151. package/server/websocket/sync.js +102 -0
  152. package/server.js +404 -0
  153. package/utils/detect-tool-usage.js +93 -0
  154. package/utils/errors.js +158 -0
  155. package/utils/html-escape.js +84 -0
  156. package/utils/id-sanitize.js +94 -0
  157. package/utils/response.js +130 -0
  158. package/utils/with-retry.js +105 -0
@@ -0,0 +1,79 @@
1
+ // ============================================
2
+ // APP ENTRY POINT
3
+ // Imports all modules in correct dependency order.
4
+ // esbuild bundles this into public/dist/bundle.js
5
+ // ============================================
6
+
7
+ // === Foundation (no dependencies) ===
8
+ import './logger.js';
9
+ import './utils/constants.js';
10
+ import './utils/sanitize.js';
11
+ import './fetch-utils.js';
12
+
13
+ // === Core (depends on logger) ===
14
+ import './core.js';
15
+ import './encryption.js';
16
+ import './storage.js';
17
+
18
+ // === UI Rendering (depends on core, storage) ===
19
+ import './errors.js';
20
+ import './utils/sse-parser.js';
21
+ import './markdown.js';
22
+ import './message-renderer.js';
23
+ import './streaming-handler.js';
24
+
25
+ // === Audio & Queues ===
26
+ import './audio-queue.js';
27
+ import './offline-queue.js';
28
+ import './file-handler.js';
29
+
30
+ // === Event Bus (cross-module communication) ===
31
+ import './event-bus.js';
32
+
33
+ // === Core App (depends on rendering + storage) ===
34
+ import './onboarding.js';
35
+ import './ui.js';
36
+ import './chat.js';
37
+ import './connection-api.js';
38
+ import './connection.js';
39
+
40
+ // === Feature Modules (deferred in original, now bundled) ===
41
+ import './panels.js';
42
+ import './voice.js';
43
+ import './realtime-voice.js';
44
+ import './appearance-settings.js';
45
+ import './tts-settings.js';
46
+ import './stt-settings.js';
47
+ import './settings.js';
48
+ import './commands.js';
49
+ import './developer.js';
50
+ import './dashboard.js';
51
+ import './files.js';
52
+ import './themes.js';
53
+ import './premium.js';
54
+ import './theme-generator.js';
55
+ import './notifications.js';
56
+ import './shortcuts.js';
57
+ import './missed-messages.js';
58
+ import './timestamps.js';
59
+ import './gateway-chat.js';
60
+ import './agents-data.js';
61
+ import './agents-ui.js';
62
+ import './agents.js';
63
+ import './satellite-sync.js';
64
+ import './satellite-ui.js';
65
+ import './satellites.js';
66
+ import './message-actions.js';
67
+ import './context-tracker.js';
68
+ import './artifacts.js';
69
+ import './splitview.js';
70
+ import './split-resize.js';
71
+ import './split-chat.js';
72
+ import './update-notifier.js';
73
+
74
+ // === Bootstrap (ensures everything is ready) ===
75
+ import './bootstrap.js';
76
+
77
+ // NOTE: mobile-debug.js is intentionally excluded from the bundle.
78
+ // It is conditionally loaded only on non-localhost connections
79
+ // and is commented out in index.html by default.
@@ -0,0 +1,111 @@
1
+ // ============================================
2
+ // APPEARANCE SETTINGS MODULE
3
+ // Theme, text size, and visual display settings
4
+ // ============================================
5
+
6
+ // DOM elements
7
+ let themeSelect;
8
+ let textSizeSelect;
9
+
10
+ // Settings
11
+ let textSize = 'medium';
12
+
13
+ function init() {
14
+ themeSelect = document.getElementById('themeSelect');
15
+ textSizeSelect = document.getElementById('textSizeSelect');
16
+
17
+ loadSettings();
18
+ setupEvents();
19
+ }
20
+
21
+ function loadSettings() {
22
+ const storage = window.UplinkStorage;
23
+ if (!storage) return;
24
+
25
+ const saved = storage.loadSettings();
26
+ if (saved.textSize) textSize = saved.textSize;
27
+ if (saved.theme) applyTheme(saved.theme);
28
+ }
29
+
30
+ function applyTheme(theme) {
31
+ document.documentElement.setAttribute('data-theme', theme);
32
+ if (themeSelect) {
33
+ // Ensure 'custom' option exists if needed
34
+ if (theme === 'custom' && !themeSelect.querySelector('option[value="custom"]')) {
35
+ var opt = document.createElement('option');
36
+ opt.value = 'custom';
37
+ opt.textContent = '✦ Custom';
38
+ themeSelect.appendChild(opt);
39
+ }
40
+ themeSelect.value = theme;
41
+ }
42
+ }
43
+
44
+ function setupEvents() {
45
+ // Theme change
46
+ themeSelect?.addEventListener('change', () => {
47
+ const theme = themeSelect.value;
48
+ // Use UplinkThemes if available (handles premium check + events)
49
+ if (window.UplinkThemes) {
50
+ window.UplinkThemes.apply(theme);
51
+ } else {
52
+ applyTheme(theme);
53
+ }
54
+ const storage = window.UplinkStorage;
55
+ if (storage) storage.saveSettings({ theme });
56
+ });
57
+
58
+ // Text size change
59
+ textSizeSelect?.addEventListener('change', () => {
60
+ textSize = textSizeSelect.value;
61
+ applyTextSize();
62
+ saveSettings();
63
+ });
64
+ }
65
+
66
+ function applyTextSize() {
67
+ const messages = document.getElementById('messages');
68
+ if (messages) {
69
+ messages.classList.remove('text-small', 'text-medium', 'text-large');
70
+ messages.classList.add(`text-${textSize}`);
71
+ }
72
+ }
73
+
74
+ function saveSettings() {
75
+ const storage = window.UplinkStorage;
76
+ if (!storage) return;
77
+
78
+ storage.saveSettings({
79
+ textSize,
80
+ mode: window.UplinkCore?.mode || 'text'
81
+ });
82
+ }
83
+
84
+ function applyState() {
85
+ // Update text size select
86
+ if (textSizeSelect) textSizeSelect.value = textSize;
87
+
88
+ // Sync theme select with current theme
89
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'midnight';
90
+ if (themeSelect) themeSelect.value = currentTheme;
91
+
92
+ applyTextSize();
93
+ }
94
+
95
+ // Expose API
96
+ export const UplinkAppearanceSettings = {
97
+ init,
98
+ applyState,
99
+ applyTheme,
100
+ getTextSize: () => textSize,
101
+ setTextSize: (size) => {
102
+ textSize = size;
103
+ applyTextSize();
104
+ saveSettings();
105
+ }
106
+ };
107
+
108
+ import { UplinkCore } from './core.js';
109
+
110
+ // Backward compat: assign to window
111
+ window.UplinkAppearanceSettings = UplinkAppearanceSettings;
@@ -0,0 +1,432 @@
1
+ // ============================================
2
+ // ARTIFACTS MODULE
3
+ // Artifacts viewer panel for agent-generated documents
4
+ // ============================================
5
+
6
+ import { UplinkCore } from './core.js';
7
+ import { UplinkMarkdown } from './markdown.js';
8
+
9
+ // DOM elements
10
+ let artifactsBtn, artifactsPanel;
11
+ let artifactsList, artifactsReader, artifactsSearch;
12
+ let readerTitle, readerContent, readerBackBtn;
13
+
14
+ // State
15
+ let artifacts = [];
16
+ let currentArtifact = null;
17
+
18
+ // AbortController for event listeners cleanup
19
+ let eventsAbortController = null;
20
+
21
+ function init() {
22
+ artifactsBtn = document.getElementById('artifactsBtn');
23
+ artifactsPanel = document.getElementById('artifactsPanel');
24
+ artifactsList = document.getElementById('artifactsList');
25
+ artifactsReader = document.getElementById('artifactsReader');
26
+ artifactsSearch = document.getElementById('artifactsSearch');
27
+ readerTitle = document.getElementById('readerTitle');
28
+ readerContent = document.getElementById('readerContent');
29
+ readerBackBtn = document.getElementById('readerBackBtn');
30
+
31
+ if (!artifactsBtn || !artifactsPanel) {
32
+ console.warn('Artifacts: Elements not found, retrying...');
33
+ setTimeout(init, 100);
34
+ return;
35
+ }
36
+
37
+ setupEvents();
38
+
39
+ // Register with panel manager
40
+ if (window.UplinkPanels && artifactsPanel) {
41
+ window.UplinkPanels.register('artifacts', {
42
+ element: artifactsPanel,
43
+ isOpen: () => artifactsPanel.classList.contains('visible'),
44
+ open: () => {
45
+ artifactsPanel.classList.add('visible');
46
+ onPanelOpen();
47
+ },
48
+ close: () => {
49
+ artifactsPanel.classList.remove('visible');
50
+ onPanelClose();
51
+ }
52
+ });
53
+ }
54
+
55
+ console.log('Artifacts: Initialized');
56
+ }
57
+
58
+ function setupEvents() {
59
+ // Abort previous event listeners
60
+ if (eventsAbortController) {
61
+ eventsAbortController.abort();
62
+ }
63
+ eventsAbortController = new AbortController();
64
+ const signal = eventsAbortController.signal;
65
+
66
+ // Toggle artifacts panel
67
+ artifactsBtn?.addEventListener('click', () => {
68
+ if (window.UplinkPanels) {
69
+ window.UplinkPanels.toggle('artifacts');
70
+ } else {
71
+ artifactsPanel?.classList.toggle('visible');
72
+ }
73
+ }, { signal });
74
+
75
+ // Close button
76
+ const closeBtn = document.getElementById('artifactsCloseBtn');
77
+ closeBtn?.addEventListener('click', () => {
78
+ if (window.UplinkPanels) {
79
+ window.UplinkPanels.close('artifacts');
80
+ } else {
81
+ artifactsPanel?.classList.remove('visible');
82
+ }
83
+ }, { signal });
84
+
85
+ // Back button
86
+ readerBackBtn?.addEventListener('click', showList, { signal });
87
+
88
+ // Download button
89
+ const downloadBtn = document.getElementById('readerDownloadBtn');
90
+ downloadBtn?.addEventListener('click', downloadCurrentArtifact, { signal });
91
+
92
+ // Search input
93
+ artifactsSearch?.addEventListener('input', filterArtifacts, { signal });
94
+
95
+ // Refresh button
96
+ const refreshBtn = document.getElementById('artifactsRefreshBtn');
97
+ refreshBtn?.addEventListener('click', loadArtifacts, { signal });
98
+ }
99
+
100
+ async function onPanelOpen() {
101
+ // Load artifacts when panel opens
102
+ await loadArtifacts();
103
+ showList();
104
+ }
105
+
106
+ function onPanelClose() {
107
+ // Return focus to artifacts button
108
+ if (artifactsBtn) {
109
+ artifactsBtn.focus();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Load artifacts list from API
115
+ */
116
+ async function loadArtifacts() {
117
+ try {
118
+ const response = await fetch('/api/artifacts');
119
+ if (!response.ok) {
120
+ throw new Error(`HTTP ${response.status}`);
121
+ }
122
+
123
+ artifacts = await response.json();
124
+ renderList();
125
+ } catch (error) {
126
+ console.error('Artifacts: Failed to load', error);
127
+ showError('Failed to load artifacts');
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Render artifacts list
133
+ */
134
+ function renderList() {
135
+ if (!artifactsList) return;
136
+
137
+ if (artifacts.length === 0) {
138
+ artifactsList.innerHTML = `
139
+ <div class="artifacts-empty">
140
+ <svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
141
+ <path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
142
+ </svg>
143
+ <p>No artifacts yet</p>
144
+ <small>Agent-generated documents will appear here</small>
145
+ </div>
146
+ `;
147
+ return;
148
+ }
149
+
150
+ const searchTerm = artifactsSearch?.value.toLowerCase() || '';
151
+ const filteredArtifacts = searchTerm
152
+ ? artifacts.filter(a => a.name.toLowerCase().includes(searchTerm))
153
+ : artifacts;
154
+
155
+ if (filteredArtifacts.length === 0) {
156
+ artifactsList.innerHTML = `
157
+ <div class="artifacts-empty">
158
+ <p>No matching artifacts</p>
159
+ </div>
160
+ `;
161
+ return;
162
+ }
163
+
164
+ artifactsList.innerHTML = filteredArtifacts.map(artifact => `
165
+ <div class="artifact-item" data-name="${escapeAttr(artifact.name)}">
166
+ <div class="artifact-icon">${getFileIcon(artifact.extension)}</div>
167
+ <div class="artifact-info">
168
+ <div class="artifact-name">${escapeHtml(artifact.name)}</div>
169
+ <div class="artifact-meta">
170
+ ${formatFileSize(artifact.size)} · ${formatRelativeTime(artifact.modified)}
171
+ </div>
172
+ </div>
173
+ </div>
174
+ `).join('');
175
+
176
+ // Add click handlers
177
+ artifactsList.querySelectorAll('.artifact-item').forEach(item => {
178
+ item.addEventListener('click', () => {
179
+ const name = item.dataset.name;
180
+ const artifact = artifacts.find(a => a.name === name);
181
+ if (artifact) {
182
+ openArtifact(artifact);
183
+ }
184
+ });
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Filter artifacts by search term
190
+ */
191
+ function filterArtifacts() {
192
+ renderList();
193
+ }
194
+
195
+ /**
196
+ * Open artifact in reader view
197
+ */
198
+ async function openArtifact(artifact) {
199
+ try {
200
+ const response = await fetch(`/api/artifacts/${encodeURIComponent(artifact.name)}`);
201
+ if (!response.ok) {
202
+ throw new Error(`HTTP ${response.status}`);
203
+ }
204
+
205
+ const data = await response.json();
206
+ currentArtifact = data;
207
+
208
+ // Render in reader
209
+ if (readerTitle) {
210
+ readerTitle.textContent = data.name;
211
+ }
212
+
213
+ if (readerContent) {
214
+ if (data.extension === '.md') {
215
+ // Render markdown
216
+ readerContent.innerHTML = UplinkMarkdown.render(data.content);
217
+ if (UplinkMarkdown.highlightCode) {
218
+ UplinkMarkdown.highlightCode(readerContent);
219
+ }
220
+ } else if (data.extension === '.html') {
221
+ // HTML files get a preview/source toggle
222
+ readerContent.innerHTML = `
223
+ <div class="artifacts-html-toggle">
224
+ <button class="artifacts-toggle-btn active" data-mode="preview">Preview</button>
225
+ <button class="artifacts-toggle-btn" data-mode="source">Source</button>
226
+ </div>
227
+ <div class="artifacts-html-preview">
228
+ <iframe class="artifacts-iframe" sandbox="allow-same-origin" srcdoc="${escapeAttr(data.content)}"></iframe>
229
+ </div>
230
+ <div class="artifacts-html-source" style="display: none;">
231
+ <pre><code>${escapeHtml(data.content)}</code></pre>
232
+ </div>
233
+ `;
234
+ // Wire up toggle buttons
235
+ readerContent.querySelectorAll('.artifacts-toggle-btn').forEach(btn => {
236
+ btn.addEventListener('click', () => {
237
+ const mode = btn.dataset.mode;
238
+ const preview = readerContent.querySelector('.artifacts-html-preview');
239
+ const source = readerContent.querySelector('.artifacts-html-source');
240
+ readerContent.querySelectorAll('.artifacts-toggle-btn').forEach(b => b.classList.remove('active'));
241
+ btn.classList.add('active');
242
+ if (mode === 'preview') {
243
+ preview.style.display = 'block';
244
+ source.style.display = 'none';
245
+ } else {
246
+ preview.style.display = 'none';
247
+ source.style.display = 'block';
248
+ }
249
+ });
250
+ });
251
+ } else {
252
+ // Plain text / code
253
+ readerContent.innerHTML = `<pre>${escapeHtml(data.content)}</pre>`;
254
+ }
255
+ }
256
+
257
+ showReader();
258
+ } catch (error) {
259
+ console.error('Artifacts: Failed to open', error);
260
+ showError(`Failed to open ${artifact.name}`);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Show artifacts list view
266
+ */
267
+ function showList() {
268
+ if (artifactsList) artifactsList.style.display = 'block';
269
+ if (artifactsReader) artifactsReader.style.display = 'none';
270
+ if (artifactsSearch) artifactsSearch.style.display = 'block';
271
+ }
272
+
273
+ /**
274
+ * Show artifact reader view
275
+ */
276
+ function showReader() {
277
+ if (artifactsList) artifactsList.style.display = 'none';
278
+ if (artifactsReader) artifactsReader.style.display = 'block';
279
+ if (artifactsSearch) artifactsSearch.style.display = 'none';
280
+ }
281
+
282
+ /**
283
+ * Download the currently open artifact
284
+ */
285
+ function downloadCurrentArtifact() {
286
+ if (!currentArtifact) return;
287
+
288
+ const blob = new Blob([currentArtifact.content], { type: 'text/plain;charset=utf-8' });
289
+ const url = URL.createObjectURL(blob);
290
+ const a = document.createElement('a');
291
+ a.href = url;
292
+ a.download = currentArtifact.name;
293
+ document.body.appendChild(a);
294
+ a.click();
295
+ document.body.removeChild(a);
296
+ URL.revokeObjectURL(url);
297
+ }
298
+
299
+ /**
300
+ * Show error message
301
+ */
302
+ function showError(message) {
303
+ if (artifactsList) {
304
+ artifactsList.innerHTML = `
305
+ <div class="artifacts-error">
306
+ <p>${escapeHtml(message)}</p>
307
+ </div>
308
+ `;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Get icon for file type
314
+ */
315
+ function getFileIcon(extension) {
316
+ const icons = {
317
+ '.md': '📄',
318
+ '.txt': '📋',
319
+ '.html': '🌐',
320
+ '.json': '📊',
321
+ '.csv': '📈',
322
+ '.yml': '⚙️',
323
+ '.yaml': '⚙️',
324
+ '.xml': '📑',
325
+ '.log': '📝'
326
+ };
327
+ return icons[extension] || '📄';
328
+ }
329
+
330
+ /**
331
+ * Format file size
332
+ */
333
+ function formatFileSize(bytes) {
334
+ if (bytes < 1024) return `${bytes} B`;
335
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
336
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
337
+ }
338
+
339
+ /**
340
+ * Format relative time
341
+ */
342
+ function formatRelativeTime(isoString) {
343
+ const date = new Date(isoString);
344
+ const now = new Date();
345
+ const diffMs = now - date;
346
+ const diffMins = Math.floor(diffMs / 60000);
347
+ const diffHours = Math.floor(diffMs / 3600000);
348
+ const diffDays = Math.floor(diffMs / 86400000);
349
+
350
+ if (diffMins < 1) return 'Just now';
351
+ if (diffMins < 60) return `${diffMins}m ago`;
352
+ if (diffHours < 24) return `${diffHours}h ago`;
353
+ if (diffDays < 7) return `${diffDays}d ago`;
354
+
355
+ return date.toLocaleDateString();
356
+ }
357
+
358
+ /**
359
+ * Escape HTML
360
+ */
361
+ function escapeHtml(text) {
362
+ const div = document.createElement('div');
363
+ div.textContent = text || '';
364
+ return div.innerHTML;
365
+ }
366
+
367
+ /**
368
+ * Escape attribute value
369
+ */
370
+ function escapeAttr(text) {
371
+ return (text || '')
372
+ .replace(/&/g, '&amp;')
373
+ .replace(/"/g, '&quot;')
374
+ .replace(/'/g, '&#39;')
375
+ .replace(/</g, '&lt;')
376
+ .replace(/>/g, '&gt;');
377
+ }
378
+
379
+ /**
380
+ * Cleanup function
381
+ */
382
+ function destroy() {
383
+ if (eventsAbortController) {
384
+ eventsAbortController.abort();
385
+ eventsAbortController = null;
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Open an artifact by filename — used for deep links from chat messages
391
+ * Opens the artifacts panel and navigates directly to the document
392
+ */
393
+ async function openArtifactByName(filename) {
394
+ // Open the panel if not already open
395
+ if (window.UplinkPanels) {
396
+ window.UplinkPanels.open('artifacts');
397
+ }
398
+
399
+ // Load artifacts if we haven't yet
400
+ if (artifacts.length === 0) {
401
+ await loadArtifacts();
402
+ }
403
+
404
+ // Find the artifact
405
+ const artifact = artifacts.find(a => a.name === filename);
406
+ if (artifact) {
407
+ await openArtifact(artifact);
408
+ } else {
409
+ // Try loading fresh in case it was just created
410
+ await loadArtifacts();
411
+ const freshArtifact = artifacts.find(a => a.name === filename);
412
+ if (freshArtifact) {
413
+ await openArtifact(freshArtifact);
414
+ } else {
415
+ console.warn(`Artifacts: "${filename}" not found`);
416
+ }
417
+ }
418
+ }
419
+
420
+ // Export API
421
+ export const UplinkArtifacts = {
422
+ init,
423
+ loadArtifacts,
424
+ openArtifactByName,
425
+ destroy
426
+ };
427
+
428
+ // Backward compat: assign to window
429
+ window.UplinkArtifacts = UplinkArtifacts;
430
+
431
+ // Register and init
432
+ UplinkCore.registerModule('artifacts', init);