clay-server 2.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
@@ -0,0 +1,697 @@
1
+ import { iconHtml, refreshIcons } from './icons.js';
2
+ import { closeSidebar } from './sidebar.js';
3
+ import { closeFileViewer } from './filebrowser.js';
4
+ import { copyToClipboard } from './utils.js';
5
+ import { getTerminalTheme } from './theme.js';
6
+
7
+ var ctx;
8
+ var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
9
+ var activeTabId = null;
10
+ var isOpen = false;
11
+ var ctrlActive = false;
12
+ var isTouchDevice = "ontouchstart" in window;
13
+ var viewportHandler = null;
14
+ var resizeObserver = null;
15
+ var toolbarBound = false;
16
+ var termCtxMenu = null;
17
+
18
+ // --- Init ---
19
+ export function initTerminal(_ctx) {
20
+ ctx = _ctx;
21
+
22
+ // Close panel button
23
+ document.getElementById("terminal-close").addEventListener("click", function () {
24
+ closeTerminal();
25
+ });
26
+
27
+ // Fullscreen toggle
28
+ document.getElementById("terminal-fullscreen").addEventListener("click", function () {
29
+ var isFs = ctx.terminalContainerEl.classList.toggle("panel-fullscreen");
30
+ var icon = this.querySelector("[data-lucide]");
31
+ if (icon) {
32
+ icon.setAttribute("data-lucide", isFs ? "minimize-2" : "maximize-2");
33
+ refreshIcons();
34
+ }
35
+ fitTerminal();
36
+ });
37
+
38
+ // Header toggle button
39
+ var toggleBtn = document.getElementById("terminal-toggle-btn");
40
+ if (toggleBtn) {
41
+ toggleBtn.addEventListener("click", function () {
42
+ if (isOpen && !ctx.terminalContainerEl.classList.contains("hidden")) {
43
+ closeTerminal();
44
+ } else {
45
+ openTerminal();
46
+ }
47
+ });
48
+ }
49
+
50
+ // Sidebar terminal button
51
+ var sidebarTermBtn = document.getElementById("terminal-sidebar-btn");
52
+ if (sidebarTermBtn) {
53
+ sidebarTermBtn.addEventListener("click", function () {
54
+ closeSidebar();
55
+ openTerminal();
56
+ });
57
+ }
58
+
59
+ // New tab button
60
+ var newTabBtn = document.getElementById("terminal-new-tab");
61
+ if (newTabBtn) {
62
+ newTabBtn.addEventListener("click", function () {
63
+ createNewTab();
64
+ });
65
+ }
66
+ }
67
+
68
+ // --- Open terminal panel ---
69
+ export function openTerminal() {
70
+ var container = ctx.terminalContainerEl;
71
+
72
+ // Hide file viewer if open (also unwatches)
73
+ closeFileViewer();
74
+
75
+ container.classList.remove("hidden");
76
+ isOpen = true;
77
+
78
+ // If no tabs exist, create one
79
+ if (tabs.size === 0) {
80
+ createNewTab();
81
+ return; // createNewTab will handle the rest via term_created
82
+ }
83
+
84
+ // Attach to active tab (or first available)
85
+ if (!activeTabId || !tabs.has(activeTabId)) {
86
+ activeTabId = tabs.keys().next().value;
87
+ }
88
+
89
+ activateTab(activeTabId);
90
+
91
+ // Mobile: close sidebar
92
+ if (window.innerWidth <= 768) {
93
+ closeSidebar();
94
+ }
95
+
96
+ refreshIcons();
97
+ }
98
+
99
+ // --- Close terminal panel (hide, detach, but keep PTYs alive) ---
100
+ export function closeTerminal() {
101
+ var container = ctx.terminalContainerEl;
102
+ container.classList.remove("panel-fullscreen");
103
+ container.classList.add("hidden");
104
+ // Reset fullscreen icon
105
+ var fsIcon = document.querySelector("#terminal-fullscreen [data-lucide]");
106
+ if (fsIcon) {
107
+ fsIcon.setAttribute("data-lucide", "maximize-2");
108
+ refreshIcons();
109
+ }
110
+
111
+ // Detach from active tab
112
+ if (activeTabId && ctx.ws && ctx.connected) {
113
+ ctx.ws.send(JSON.stringify({ type: "term_detach", id: activeTabId }));
114
+ }
115
+
116
+ cleanupListeners();
117
+
118
+ // Hide toolbar
119
+ var toolbar = document.getElementById("terminal-toolbar");
120
+ if (toolbar) {
121
+ toolbar.classList.add("hidden");
122
+ var ctrlBtn = toolbar.querySelector("[data-key='ctrl']");
123
+ if (ctrlBtn) ctrlBtn.classList.remove("active");
124
+ }
125
+ ctrlActive = false;
126
+
127
+ isOpen = false;
128
+ }
129
+
130
+ // --- Create new tab ---
131
+ function createNewTab() {
132
+ if (!ctx.ws || !ctx.connected) return;
133
+
134
+ // Get current terminal body dimensions for cols/rows
135
+ var cols = 80;
136
+ var rows = 24;
137
+ if (activeTabId && tabs.has(activeTabId)) {
138
+ var activeTab = tabs.get(activeTabId);
139
+ if (activeTab.xterm) {
140
+ cols = activeTab.xterm.cols || 80;
141
+ rows = activeTab.xterm.rows || 24;
142
+ }
143
+ }
144
+
145
+ ctx.ws.send(JSON.stringify({ type: "term_create", cols: cols, rows: rows }));
146
+ }
147
+
148
+ // --- Close a tab (kill PTY) ---
149
+ function closeTab(termId) {
150
+ if (!ctx.ws || !ctx.connected) return;
151
+ ctx.ws.send(JSON.stringify({ type: "term_close", id: termId }));
152
+ }
153
+
154
+ // --- Activate a tab (show xterm, attach) ---
155
+ function activateTab(termId) {
156
+ var tab = tabs.get(termId);
157
+ if (!tab) return;
158
+
159
+ // Detach from old active
160
+ if (activeTabId && activeTabId !== termId && ctx.ws && ctx.connected) {
161
+ ctx.ws.send(JSON.stringify({ type: "term_detach", id: activeTabId }));
162
+ }
163
+
164
+ // Hide all tab bodies
165
+ for (var t of tabs.values()) {
166
+ if (t.bodyEl) t.bodyEl.style.display = "none";
167
+ }
168
+
169
+ activeTabId = termId;
170
+
171
+ // Lazy-create xterm instance
172
+ if (!tab.xterm) {
173
+ createXtermForTab(tab);
174
+ }
175
+
176
+ // Show this tab's body
177
+ if (tab.bodyEl) tab.bodyEl.style.display = "";
178
+
179
+ // Attach to server
180
+ if (ctx.ws && ctx.connected) {
181
+ ctx.ws.send(JSON.stringify({ type: "term_attach", id: termId }));
182
+ }
183
+
184
+ // Fit and focus
185
+ setupListeners();
186
+ fitTerminal();
187
+
188
+ if (tab.xterm) {
189
+ tab.xterm.focus();
190
+ }
191
+
192
+ // Show toolbar on touch devices
193
+ var toolbar = document.getElementById("terminal-toolbar");
194
+ if (toolbar && isTouchDevice) {
195
+ toolbar.classList.remove("hidden");
196
+ initToolbar(toolbar);
197
+ }
198
+
199
+ // Mobile viewport handling
200
+ if (window.visualViewport && !viewportHandler) {
201
+ viewportHandler = function () {
202
+ ctx.terminalContainerEl.style.height = window.visualViewport.height + "px";
203
+ fitTerminal();
204
+ };
205
+ window.visualViewport.addEventListener("resize", viewportHandler);
206
+ }
207
+
208
+ renderTabBar();
209
+ }
210
+
211
+ // --- Create xterm.js instance for a tab ---
212
+ function createXtermForTab(tab) {
213
+ if (typeof Terminal === "undefined") return;
214
+
215
+ var xterm = new Terminal({
216
+ cursorBlink: true,
217
+ fontSize: 13,
218
+ fontFamily: "'SF Mono', Menlo, Monaco, 'Courier New', monospace",
219
+ theme: getTerminalTheme(),
220
+ });
221
+
222
+ var fitAddon = null;
223
+ if (typeof FitAddon !== "undefined") {
224
+ fitAddon = new FitAddon.FitAddon();
225
+ xterm.loadAddon(fitAddon);
226
+ }
227
+
228
+ // Create a container div for this tab's terminal
229
+ var bodyEl = document.createElement("div");
230
+ bodyEl.className = "terminal-tab-body";
231
+ ctx.terminalBodyEl.appendChild(bodyEl);
232
+
233
+ xterm.open(bodyEl);
234
+
235
+ // Route input to server
236
+ xterm.onData(function (data) {
237
+ if (ctx.ws && ctx.connected) {
238
+ ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: data }));
239
+ }
240
+ });
241
+
242
+ // Right-click context menu
243
+ bodyEl.addEventListener("contextmenu", function (e) {
244
+ showTermCtxMenu(e, tab);
245
+ });
246
+
247
+ tab.xterm = xterm;
248
+ tab.fitAddon = fitAddon;
249
+ tab.bodyEl = bodyEl;
250
+ }
251
+
252
+ // --- Fit active terminal ---
253
+ function fitTerminal() {
254
+ if (!activeTabId) return;
255
+ var tab = tabs.get(activeTabId);
256
+ if (!tab || !tab.fitAddon || !tab.xterm) return;
257
+
258
+ try {
259
+ tab.fitAddon.fit();
260
+ if (ctx.ws && ctx.connected) {
261
+ ctx.ws.send(JSON.stringify({
262
+ type: "term_resize",
263
+ id: activeTabId,
264
+ cols: tab.xterm.cols,
265
+ rows: tab.xterm.rows,
266
+ }));
267
+ }
268
+ } catch (e) {}
269
+ }
270
+
271
+ // --- Setup/cleanup resize listeners ---
272
+ function setupListeners() {
273
+ cleanupListeners();
274
+
275
+ window.addEventListener("resize", fitTerminal);
276
+
277
+ if (typeof ResizeObserver !== "undefined" && ctx.terminalBodyEl) {
278
+ resizeObserver = new ResizeObserver(function () {
279
+ fitTerminal();
280
+ });
281
+ resizeObserver.observe(ctx.terminalBodyEl);
282
+ }
283
+ }
284
+
285
+ function cleanupListeners() {
286
+ window.removeEventListener("resize", fitTerminal);
287
+
288
+ if (resizeObserver) {
289
+ resizeObserver.disconnect();
290
+ resizeObserver = null;
291
+ }
292
+
293
+ if (viewportHandler && window.visualViewport) {
294
+ window.visualViewport.removeEventListener("resize", viewportHandler);
295
+ viewportHandler = null;
296
+ }
297
+ ctx.terminalContainerEl.style.height = "";
298
+ }
299
+
300
+ // --- Render tab bar ---
301
+ function renderTabBar() {
302
+ var tabsEl = document.getElementById("terminal-tabs");
303
+ if (!tabsEl) return;
304
+
305
+ tabsEl.innerHTML = "";
306
+
307
+ for (var tab of tabs.values()) {
308
+ (function (t) {
309
+ var el = document.createElement("div");
310
+ el.className = "terminal-tab";
311
+ if (t.id === activeTabId) el.classList.add("active");
312
+ if (t.exited) el.classList.add("exited");
313
+
314
+ var label = document.createElement("span");
315
+ label.className = "terminal-tab-label";
316
+ label.textContent = t.title;
317
+ el.appendChild(label);
318
+
319
+ // Double-click label to rename
320
+ label.addEventListener("dblclick", function (e) {
321
+ e.stopPropagation();
322
+ startRenameTab(t, label);
323
+ });
324
+
325
+ var closeBtn = document.createElement("button");
326
+ closeBtn.className = "terminal-tab-close";
327
+ closeBtn.innerHTML = '<i data-lucide="trash-2" style="width:12px;height:12px"></i>';
328
+ closeBtn.addEventListener("click", function (e) {
329
+ e.stopPropagation();
330
+ closeTab(t.id);
331
+ });
332
+ el.appendChild(closeBtn);
333
+
334
+ el.addEventListener("click", function () {
335
+ if (t.id !== activeTabId) {
336
+ activateTab(t.id);
337
+ }
338
+ });
339
+
340
+ tabsEl.appendChild(el);
341
+ })(tab);
342
+ }
343
+
344
+ updateTerminalBadge();
345
+ refreshIcons();
346
+ }
347
+
348
+ // --- Rename tab inline ---
349
+ function startRenameTab(tab, labelEl) {
350
+ var input = document.createElement("input");
351
+ input.className = "terminal-tab-rename";
352
+ input.value = tab.title;
353
+ input.maxLength = 50;
354
+
355
+ labelEl.replaceWith(input);
356
+ input.focus();
357
+ input.select();
358
+
359
+ function commit() {
360
+ var newTitle = input.value.trim();
361
+ if (newTitle && newTitle !== tab.title) {
362
+ tab.title = newTitle;
363
+ if (ctx.ws && ctx.connected) {
364
+ ctx.ws.send(JSON.stringify({ type: "term_rename", id: tab.id, title: newTitle }));
365
+ }
366
+ }
367
+ renderTabBar();
368
+ }
369
+
370
+ input.addEventListener("blur", commit);
371
+ input.addEventListener("keydown", function (e) {
372
+ if (e.key === "Enter") { input.blur(); }
373
+ if (e.key === "Escape") {
374
+ input.value = tab.title; // revert
375
+ input.blur();
376
+ }
377
+ e.stopPropagation();
378
+ });
379
+ }
380
+
381
+ // --- Update terminal count badge ---
382
+ function updateTerminalBadge() {
383
+ var countEl = document.getElementById("terminal-count");
384
+ var sidebarCountEl = document.getElementById("terminal-sidebar-count");
385
+
386
+ var count = 0;
387
+ for (var t of tabs.values()) {
388
+ if (!t.exited) count++;
389
+ }
390
+
391
+ if (countEl) {
392
+ if (count > 0) {
393
+ countEl.textContent = count;
394
+ countEl.classList.remove("hidden");
395
+ } else {
396
+ countEl.classList.add("hidden");
397
+ }
398
+ }
399
+
400
+ if (sidebarCountEl) {
401
+ if (count > 0) {
402
+ sidebarCountEl.textContent = count;
403
+ sidebarCountEl.classList.remove("hidden");
404
+ } else {
405
+ sidebarCountEl.classList.add("hidden");
406
+ }
407
+ }
408
+
409
+ var mobileCountEl = document.getElementById("mobile-terminal-count");
410
+ if (mobileCountEl) {
411
+ if (count > 0) {
412
+ mobileCountEl.textContent = count;
413
+ mobileCountEl.classList.remove("hidden");
414
+ } else {
415
+ mobileCountEl.classList.add("hidden");
416
+ }
417
+ }
418
+ }
419
+
420
+ // --- Handle server messages ---
421
+
422
+ export function handleTermList(msg) {
423
+ var serverTerminals = msg.terminals || [];
424
+ var serverIds = new Set();
425
+
426
+ // Add/update tabs from server list
427
+ for (var i = 0; i < serverTerminals.length; i++) {
428
+ var st = serverTerminals[i];
429
+ serverIds.add(st.id);
430
+
431
+ if (tabs.has(st.id)) {
432
+ var existing = tabs.get(st.id);
433
+ existing.title = st.title;
434
+ existing.exited = st.exited;
435
+ } else {
436
+ tabs.set(st.id, {
437
+ id: st.id,
438
+ title: st.title,
439
+ exited: st.exited,
440
+ xterm: null,
441
+ fitAddon: null,
442
+ bodyEl: null,
443
+ });
444
+ }
445
+ }
446
+
447
+ // Remove tabs no longer on server
448
+ for (var id of tabs.keys()) {
449
+ if (!serverIds.has(id)) {
450
+ var removed = tabs.get(id);
451
+ if (removed.xterm) {
452
+ removed.xterm.dispose();
453
+ }
454
+ if (removed.bodyEl && removed.bodyEl.parentNode) {
455
+ removed.bodyEl.parentNode.removeChild(removed.bodyEl);
456
+ }
457
+ tabs.delete(id);
458
+ }
459
+ }
460
+
461
+ // If active tab was removed, switch to first available
462
+ if (activeTabId && !tabs.has(activeTabId)) {
463
+ activeTabId = null;
464
+ }
465
+
466
+ renderTabBar();
467
+
468
+ // If panel is open and we have tabs, re-attach
469
+ if (isOpen && tabs.size > 0) {
470
+ if (!activeTabId) {
471
+ activeTabId = tabs.keys().next().value;
472
+ }
473
+ activateTab(activeTabId);
474
+ }
475
+
476
+ // If panel is open and all tabs are gone, close panel
477
+ if (isOpen && tabs.size === 0) {
478
+ closeTerminal();
479
+ }
480
+ }
481
+
482
+ export function handleTermCreated(msg) {
483
+ // Switch to the newly created tab
484
+ if (msg.id && tabs.has(msg.id)) {
485
+ activateTab(msg.id);
486
+ }
487
+ }
488
+
489
+ export function handleTermOutput(msg) {
490
+ if (!msg.id) return;
491
+ var tab = tabs.get(msg.id);
492
+ if (tab && tab.xterm && msg.data) {
493
+ tab.xterm.write(msg.data);
494
+ }
495
+ }
496
+
497
+ export function handleTermExited(msg) {
498
+ if (!msg.id) return;
499
+ var tab = tabs.get(msg.id);
500
+ if (tab) {
501
+ tab.exited = true;
502
+ if (tab.xterm) {
503
+ tab.xterm.write("\r\n\x1b[90m[Process exited]\x1b[0m\r\n");
504
+ }
505
+ renderTabBar();
506
+ }
507
+ }
508
+
509
+ export function handleTermClosed(msg) {
510
+ if (!msg.id) return;
511
+ var tab = tabs.get(msg.id);
512
+ if (tab) {
513
+ if (tab.xterm) tab.xterm.dispose();
514
+ if (tab.bodyEl && tab.bodyEl.parentNode) {
515
+ tab.bodyEl.parentNode.removeChild(tab.bodyEl);
516
+ }
517
+ tabs.delete(msg.id);
518
+
519
+ if (activeTabId === msg.id) {
520
+ activeTabId = null;
521
+ if (tabs.size > 0) {
522
+ activeTabId = tabs.keys().next().value;
523
+ activateTab(activeTabId);
524
+ }
525
+ }
526
+
527
+ renderTabBar();
528
+
529
+ // Close panel if no tabs left
530
+ if (isOpen && tabs.size === 0) {
531
+ closeTerminal();
532
+ }
533
+ }
534
+ }
535
+
536
+ // --- Reset on reconnect ---
537
+ export function resetTerminals() {
538
+ // Dispose all xterm instances (server state survives, client re-syncs via term_list)
539
+ for (var tab of tabs.values()) {
540
+ if (tab.xterm) {
541
+ tab.xterm.dispose();
542
+ tab.xterm = null;
543
+ tab.fitAddon = null;
544
+ }
545
+ if (tab.bodyEl && tab.bodyEl.parentNode) {
546
+ tab.bodyEl.parentNode.removeChild(tab.bodyEl);
547
+ tab.bodyEl = null;
548
+ }
549
+ }
550
+ tabs.clear();
551
+ activeTabId = null;
552
+ cleanupListeners();
553
+ renderTabBar();
554
+ }
555
+
556
+ export function setTerminalTheme(xtermTheme) {
557
+ for (var tab of tabs.values()) {
558
+ if (tab.xterm) {
559
+ tab.xterm.options.theme = xtermTheme;
560
+ }
561
+ }
562
+ }
563
+
564
+ // --- Terminal context menu ---
565
+ function closeTermCtxMenu() {
566
+ if (termCtxMenu) {
567
+ termCtxMenu.remove();
568
+ termCtxMenu = null;
569
+ }
570
+ }
571
+
572
+ function showTermCtxMenu(e, tab) {
573
+ e.preventDefault();
574
+ e.stopPropagation();
575
+ closeTermCtxMenu();
576
+
577
+ var menu = document.createElement("div");
578
+ menu.className = "term-ctx-menu";
579
+
580
+ // Copy
581
+ var copyItem = document.createElement("button");
582
+ copyItem.className = "term-ctx-item";
583
+ copyItem.innerHTML = iconHtml("clipboard-copy") + " <span>Copy Terminal</span>";
584
+ copyItem.addEventListener("click", function (ev) {
585
+ ev.stopPropagation();
586
+ closeTermCtxMenu();
587
+ if (!tab.xterm) return;
588
+ tab.xterm.selectAll();
589
+ var text = tab.xterm.getSelection();
590
+ tab.xterm.clearSelection();
591
+ if (text) copyToClipboard(text);
592
+ });
593
+ menu.appendChild(copyItem);
594
+
595
+ // Clear
596
+ var clearItem = document.createElement("button");
597
+ clearItem.className = "term-ctx-item";
598
+ clearItem.innerHTML = iconHtml("trash-2") + " <span>Clear Terminal</span>";
599
+ clearItem.addEventListener("click", function (ev) {
600
+ ev.stopPropagation();
601
+ closeTermCtxMenu();
602
+ if (!tab.xterm) return;
603
+ tab.xterm.clear();
604
+ });
605
+ menu.appendChild(clearItem);
606
+
607
+ // Position at mouse cursor
608
+ menu.style.left = e.clientX + "px";
609
+ menu.style.top = e.clientY + "px";
610
+ document.body.appendChild(menu);
611
+
612
+ // Clamp to viewport
613
+ var rect = menu.getBoundingClientRect();
614
+ if (rect.right > window.innerWidth) {
615
+ menu.style.left = (window.innerWidth - rect.width - 4) + "px";
616
+ }
617
+ if (rect.bottom > window.innerHeight) {
618
+ menu.style.top = (window.innerHeight - rect.height - 4) + "px";
619
+ }
620
+
621
+ termCtxMenu = menu;
622
+ refreshIcons();
623
+
624
+ // Close on outside click (next tick to avoid immediate trigger)
625
+ setTimeout(function () {
626
+ document.addEventListener("click", closeTermCtxMenu, { once: true });
627
+ }, 0);
628
+ }
629
+
630
+ // --- Mobile toolbar ---
631
+ var KEY_MAP = {
632
+ tab: "\t",
633
+ esc: "\x1b",
634
+ up: "\x1b[A",
635
+ down: "\x1b[B",
636
+ right: "\x1b[C",
637
+ left: "\x1b[D",
638
+ };
639
+
640
+ function initToolbar(toolbar) {
641
+ if (!toolbarBound) {
642
+ toolbarBound = true;
643
+
644
+ toolbar.addEventListener("mousedown", function (e) { e.preventDefault(); });
645
+
646
+ toolbar.addEventListener("click", function (e) {
647
+ var btn = e.target.closest(".term-key");
648
+ if (!btn) return;
649
+
650
+ var tab = activeTabId ? tabs.get(activeTabId) : null;
651
+ if (!tab || !tab.xterm) return;
652
+
653
+ var key = btn.dataset.key;
654
+ if (!key) return;
655
+
656
+ if (key === "ctrl") {
657
+ ctrlActive = !ctrlActive;
658
+ btn.classList.toggle("active", ctrlActive);
659
+ return;
660
+ }
661
+
662
+ var seq = KEY_MAP[key];
663
+ if (!seq) return;
664
+
665
+ if (ctx.ws && ctx.connected) {
666
+ ctx.ws.send(JSON.stringify({ type: "term_input", id: activeTabId, data: seq }));
667
+ }
668
+
669
+ if (ctrlActive) {
670
+ ctrlActive = false;
671
+ var ctrlBtn = toolbar.querySelector("[data-key='ctrl']");
672
+ if (ctrlBtn) ctrlBtn.classList.remove("active");
673
+ }
674
+ });
675
+ }
676
+
677
+ // Attach Ctrl handler to active terminal
678
+ var tab = activeTabId ? tabs.get(activeTabId) : null;
679
+ if (tab && tab.xterm) {
680
+ tab.xterm.attachCustomKeyEventHandler(function (ev) {
681
+ if (ctrlActive && ev.type === "keydown" && ev.key.length === 1) {
682
+ var charCode = ev.key.toUpperCase().charCodeAt(0);
683
+ if (charCode >= 65 && charCode <= 90) {
684
+ var ctrlChar = String.fromCharCode(charCode - 64);
685
+ if (ctx.ws && ctx.connected) {
686
+ ctx.ws.send(JSON.stringify({ type: "term_input", id: activeTabId, data: ctrlChar }));
687
+ }
688
+ ctrlActive = false;
689
+ var ctrlBtn = document.querySelector("#terminal-toolbar [data-key='ctrl']");
690
+ if (ctrlBtn) ctrlBtn.classList.remove("active");
691
+ return false;
692
+ }
693
+ }
694
+ return true;
695
+ });
696
+ }
697
+ }