claude-relay 2.4.2 → 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 (75) hide show
  1. package/bin/cli.js +1 -2350
  2. package/package.json +7 -42
  3. package/LICENSE +0 -21
  4. package/README.md +0 -281
  5. package/lib/cli-sessions.js +0 -270
  6. package/lib/config.js +0 -222
  7. package/lib/daemon.js +0 -423
  8. package/lib/ipc.js +0 -112
  9. package/lib/pages.js +0 -714
  10. package/lib/project.js +0 -1224
  11. package/lib/public/app.js +0 -2157
  12. package/lib/public/apple-touch-icon.png +0 -0
  13. package/lib/public/css/base.css +0 -145
  14. package/lib/public/css/diff.css +0 -128
  15. package/lib/public/css/filebrowser.css +0 -1076
  16. package/lib/public/css/highlight.css +0 -144
  17. package/lib/public/css/input.css +0 -512
  18. package/lib/public/css/menus.css +0 -683
  19. package/lib/public/css/messages.css +0 -1159
  20. package/lib/public/css/overlays.css +0 -731
  21. package/lib/public/css/rewind.css +0 -529
  22. package/lib/public/css/sidebar.css +0 -794
  23. package/lib/public/favicon.svg +0 -26
  24. package/lib/public/icon-192.png +0 -0
  25. package/lib/public/icon-512.png +0 -0
  26. package/lib/public/icon-mono.svg +0 -19
  27. package/lib/public/index.html +0 -460
  28. package/lib/public/manifest.json +0 -27
  29. package/lib/public/modules/diff.js +0 -398
  30. package/lib/public/modules/events.js +0 -21
  31. package/lib/public/modules/filebrowser.js +0 -1375
  32. package/lib/public/modules/fileicons.js +0 -172
  33. package/lib/public/modules/icons.js +0 -54
  34. package/lib/public/modules/input.js +0 -578
  35. package/lib/public/modules/markdown.js +0 -149
  36. package/lib/public/modules/notifications.js +0 -643
  37. package/lib/public/modules/qrcode.js +0 -70
  38. package/lib/public/modules/rewind.js +0 -334
  39. package/lib/public/modules/sidebar.js +0 -628
  40. package/lib/public/modules/state.js +0 -3
  41. package/lib/public/modules/terminal.js +0 -658
  42. package/lib/public/modules/theme.js +0 -622
  43. package/lib/public/modules/tools.js +0 -1410
  44. package/lib/public/modules/utils.js +0 -56
  45. package/lib/public/style.css +0 -10
  46. package/lib/public/sw.js +0 -75
  47. package/lib/push.js +0 -125
  48. package/lib/sdk-bridge.js +0 -771
  49. package/lib/server.js +0 -577
  50. package/lib/sessions.js +0 -402
  51. package/lib/terminal-manager.js +0 -187
  52. package/lib/terminal.js +0 -24
  53. package/lib/themes/ayu-light.json +0 -9
  54. package/lib/themes/catppuccin-latte.json +0 -9
  55. package/lib/themes/catppuccin-mocha.json +0 -9
  56. package/lib/themes/claude-light.json +0 -9
  57. package/lib/themes/claude.json +0 -9
  58. package/lib/themes/dracula.json +0 -9
  59. package/lib/themes/everforest-light.json +0 -9
  60. package/lib/themes/everforest.json +0 -9
  61. package/lib/themes/github-light.json +0 -9
  62. package/lib/themes/gruvbox-dark.json +0 -9
  63. package/lib/themes/gruvbox-light.json +0 -9
  64. package/lib/themes/monokai.json +0 -9
  65. package/lib/themes/nord-light.json +0 -9
  66. package/lib/themes/nord.json +0 -9
  67. package/lib/themes/one-dark.json +0 -9
  68. package/lib/themes/one-light.json +0 -9
  69. package/lib/themes/rose-pine-dawn.json +0 -9
  70. package/lib/themes/rose-pine.json +0 -9
  71. package/lib/themes/solarized-dark.json +0 -9
  72. package/lib/themes/solarized-light.json +0 -9
  73. package/lib/themes/tokyo-night-light.json +0 -9
  74. package/lib/themes/tokyo-night.json +0 -9
  75. package/lib/updater.js +0 -96
@@ -1,578 +0,0 @@
1
- import { iconHtml, refreshIcons } from './icons.js';
2
- import { setRewindMode, isRewindMode } from './rewind.js';
3
-
4
- var ctx;
5
-
6
- // --- State ---
7
- var pendingImages = []; // [{data: base64, mediaType: "image/png"}]
8
- var pendingPastes = []; // [{text: string, preview: string}]
9
- var slashActiveIdx = -1;
10
- var slashFiltered = [];
11
- var isComposing = false;
12
- var isRemoteInput = false;
13
-
14
- export var builtinCommands = [
15
- { name: "clear", desc: "Clear conversation" },
16
- { name: "context", desc: "Context window usage" },
17
- { name: "rewind", desc: "Toggle rewind mode" },
18
- { name: "usage", desc: "Toggle usage panel" },
19
- { name: "status", desc: "Process status and resource usage" },
20
- ];
21
-
22
- // --- Send ---
23
- export function sendMessage() {
24
- var text = ctx.inputEl.value.trim();
25
- var images = pendingImages.slice();
26
- if (!text && images.length === 0 && pendingPastes.length === 0) return;
27
- hideSlashMenu();
28
-
29
- if (text === "/clear") {
30
- ctx.inputEl.value = "";
31
- clearPendingImages();
32
- autoResize();
33
- if (ctx.ws && ctx.connected) {
34
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
35
- }
36
- return;
37
- }
38
-
39
- if (text === "/rewind") {
40
- ctx.inputEl.value = "";
41
- clearPendingImages();
42
- autoResize();
43
- if (ctx.messageUuidMap().length === 0) {
44
- ctx.addSystemMessage("No rewind points available in this session.", true);
45
- } else {
46
- setRewindMode(!isRewindMode());
47
- }
48
- return;
49
- }
50
-
51
- if (text === "/context") {
52
- ctx.inputEl.value = "";
53
- clearPendingImages();
54
- autoResize();
55
- if (ctx.toggleContextPanel) ctx.toggleContextPanel();
56
- return;
57
- }
58
-
59
- if (text === "/usage") {
60
- ctx.inputEl.value = "";
61
- clearPendingImages();
62
- autoResize();
63
- if (ctx.toggleUsagePanel) ctx.toggleUsagePanel();
64
- return;
65
- }
66
-
67
- if (text === "/status") {
68
- ctx.inputEl.value = "";
69
- clearPendingImages();
70
- autoResize();
71
- if (ctx.toggleStatusPanel) ctx.toggleStatusPanel();
72
- return;
73
- }
74
-
75
- if (!ctx.connected) {
76
- ctx.addSystemMessage("Not connected — message not sent.", true);
77
- return;
78
- }
79
-
80
- var pastes = pendingPastes.map(function (p) { return p.text; });
81
- ctx.addUserMessage(text, images.length > 0 ? images : null, pastes.length > 0 ? pastes : null);
82
-
83
- var payload = { type: "message", text: text || "" };
84
- if (images.length > 0) {
85
- payload.images = images;
86
- }
87
- if (pastes.length > 0) {
88
- payload.pastes = pastes;
89
- }
90
- ctx.ws.send(JSON.stringify(payload));
91
-
92
- ctx.inputEl.value = "";
93
- sendInputSync();
94
- clearPendingImages();
95
- autoResize();
96
- ctx.inputEl.focus();
97
- }
98
-
99
- export function autoResize() {
100
- ctx.inputEl.style.height = "auto";
101
- ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";
102
- }
103
-
104
- // --- File path extraction from clipboard ---
105
- function extractFilePaths(cd) {
106
- var paths = [];
107
-
108
- // 1. Check text/uri-list for file:// URIs (Finder on some browsers)
109
- var uriList = cd.getData("text/uri-list");
110
- if (uriList) {
111
- var lines = uriList.split(/\r?\n/);
112
- for (var i = 0; i < lines.length; i++) {
113
- var line = lines[i].trim();
114
- if (line && !line.startsWith("#") && line.startsWith("file://")) {
115
- paths.push(decodeURIComponent(line.replace("file://", "")));
116
- }
117
- }
118
- if (paths.length > 0) return paths;
119
- }
120
-
121
- // 2. Check if text/plain looks like file path(s) while files are present
122
- // (Finder Cmd+C puts filename in text/plain, Cmd+Option+C puts full path)
123
- if (cd.files && cd.files.length > 0) {
124
- var plainText = cd.getData("text/plain");
125
- if (plainText) {
126
- var textLines = plainText.split(/\r?\n/).filter(function (l) { return l.trim(); });
127
- for (var i = 0; i < textLines.length; i++) {
128
- var p = textLines[i].trim();
129
- if (p.startsWith("/") || p.startsWith("~")) {
130
- paths.push(p);
131
- }
132
- }
133
- if (paths.length > 0) return paths;
134
- }
135
- // 3. Fallback: files present but no path in text, use filenames
136
- for (var i = 0; i < cd.files.length; i++) {
137
- var f = cd.files[i];
138
- if (f.name && f.type.indexOf("image/") !== 0) {
139
- paths.push(f.name);
140
- }
141
- }
142
- }
143
-
144
- return paths;
145
- }
146
-
147
- // --- Insert text at cursor in textarea ---
148
- function insertTextAtCursor(text) {
149
- var el = ctx.inputEl;
150
- el.focus();
151
- var start = el.selectionStart;
152
- var end = el.selectionEnd;
153
- var before = el.value.substring(0, start);
154
- var after = el.value.substring(end);
155
- // Add space before if cursor is right after non-space text
156
- if (before.length > 0 && before[before.length - 1] !== " " && before[before.length - 1] !== "\n") {
157
- text = " " + text;
158
- }
159
- el.value = before + text + after;
160
- el.selectionStart = el.selectionEnd = start + text.length;
161
- autoResize();
162
- sendInputSync();
163
- }
164
-
165
- // --- Image paste ---
166
- function addPendingImage(dataUrl) {
167
- var commaIdx = dataUrl.indexOf(",");
168
- if (commaIdx === -1) return;
169
- var header = dataUrl.substring(0, commaIdx);
170
- var data = dataUrl.substring(commaIdx + 1);
171
- var typeMatch = header.match(/data:(image\/[^;,]+)/);
172
- if (!typeMatch || !data) return;
173
- pendingImages.push({ mediaType: typeMatch[1], data: data });
174
- renderInputPreviews();
175
- }
176
-
177
- function removePendingImage(idx) {
178
- pendingImages.splice(idx, 1);
179
- renderInputPreviews();
180
- }
181
-
182
- export function clearPendingImages() {
183
- pendingImages = [];
184
- pendingPastes = [];
185
- renderInputPreviews();
186
- }
187
-
188
- function removePendingPaste(idx) {
189
- pendingPastes.splice(idx, 1);
190
- renderInputPreviews();
191
- }
192
-
193
- function renderInputPreviews() {
194
- var bar = ctx.imagePreviewBar;
195
- bar.innerHTML = "";
196
- if (pendingImages.length === 0 && pendingPastes.length === 0) {
197
- bar.classList.remove("visible");
198
- return;
199
- }
200
- bar.classList.add("visible");
201
-
202
- // Image thumbnails
203
- for (var i = 0; i < pendingImages.length; i++) {
204
- (function (idx) {
205
- var wrap = document.createElement("div");
206
- wrap.className = "image-preview-thumb";
207
- var img = document.createElement("img");
208
- img.src = "data:" + pendingImages[idx].mediaType + ";base64," + pendingImages[idx].data;
209
- img.addEventListener("click", function () {
210
- if (ctx.showImageModal) ctx.showImageModal(this.src);
211
- });
212
- var removeBtn = document.createElement("button");
213
- removeBtn.className = "image-preview-remove";
214
- removeBtn.innerHTML = iconHtml("x");
215
- removeBtn.addEventListener("click", function () {
216
- removePendingImage(idx);
217
- });
218
- wrap.appendChild(img);
219
- wrap.appendChild(removeBtn);
220
- bar.appendChild(wrap);
221
- })(i);
222
- }
223
-
224
- // Pasted content chips
225
- for (var j = 0; j < pendingPastes.length; j++) {
226
- (function (idx) {
227
- var chip = document.createElement("div");
228
- chip.className = "pasted-chip";
229
- var preview = document.createElement("span");
230
- preview.className = "pasted-chip-preview";
231
- preview.textContent = pendingPastes[idx].preview;
232
- var label = document.createElement("span");
233
- label.className = "pasted-chip-label";
234
- label.textContent = "PASTED";
235
- var removeBtn = document.createElement("button");
236
- removeBtn.className = "pasted-chip-remove";
237
- removeBtn.innerHTML = iconHtml("x");
238
- removeBtn.addEventListener("click", function (e) {
239
- e.stopPropagation();
240
- removePendingPaste(idx);
241
- });
242
- chip.appendChild(preview);
243
- chip.appendChild(label);
244
- chip.appendChild(removeBtn);
245
- bar.appendChild(chip);
246
- })(j);
247
- }
248
-
249
- refreshIcons();
250
- }
251
-
252
- var MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB
253
- var RESIZE_MAX_DIM = 1920;
254
- var RESIZE_QUALITY = 0.85;
255
-
256
- function readImageBlob(blob) {
257
- var reader = new FileReader();
258
- reader.onload = function (ev) {
259
- var dataUrl = ev.target.result;
260
- // Check base64 payload size (~3/4 of base64 length)
261
- var commaIdx = dataUrl.indexOf(",");
262
- var b64 = commaIdx !== -1 ? dataUrl.substring(commaIdx + 1) : "";
263
- var estimatedBytes = b64.length * 0.75;
264
-
265
- if (estimatedBytes <= MAX_IMAGE_BYTES) {
266
- addPendingImage(dataUrl);
267
- return;
268
- }
269
-
270
- // Resize via canvas
271
- var img = new Image();
272
- img.onload = function () {
273
- var w = img.naturalWidth;
274
- var h = img.naturalHeight;
275
- var scale = Math.min(RESIZE_MAX_DIM / Math.max(w, h), 1);
276
- var nw = Math.round(w * scale);
277
- var nh = Math.round(h * scale);
278
- var canvas = document.createElement("canvas");
279
- canvas.width = nw;
280
- canvas.height = nh;
281
- var cx = canvas.getContext("2d");
282
- cx.drawImage(img, 0, 0, nw, nh);
283
- var resized = canvas.toDataURL("image/jpeg", RESIZE_QUALITY);
284
- addPendingImage(resized);
285
- };
286
- img.src = dataUrl;
287
- };
288
- reader.readAsDataURL(blob);
289
- }
290
-
291
- // --- Slash menu ---
292
- function getAllCommands() {
293
- return builtinCommands.concat(ctx.slashCommands());
294
- }
295
-
296
- function showSlashMenu(filter) {
297
- var query = filter.toLowerCase();
298
- slashFiltered = getAllCommands().filter(function (c) {
299
- return c.name.toLowerCase().indexOf(query) !== -1;
300
- });
301
- if (slashFiltered.length === 0) { hideSlashMenu(); return; }
302
-
303
- slashActiveIdx = 0;
304
- ctx.slashMenu.innerHTML = slashFiltered.map(function (c, i) {
305
- return '<div class="slash-item' + (i === 0 ? ' active' : '') + '" data-idx="' + i + '">' +
306
- '<span class="slash-cmd">/' + c.name + '</span>' +
307
- '<span class="slash-desc">' + c.desc + '</span>' +
308
- '</div>';
309
- }).join("");
310
- ctx.slashMenu.classList.add("visible");
311
-
312
- ctx.slashMenu.querySelectorAll(".slash-item").forEach(function (el) {
313
- el.addEventListener("click", function () {
314
- selectSlashItem(parseInt(el.dataset.idx));
315
- });
316
- });
317
- }
318
-
319
- export function hideSlashMenu() {
320
- ctx.slashMenu.classList.remove("visible");
321
- ctx.slashMenu.innerHTML = "";
322
- slashActiveIdx = -1;
323
- slashFiltered = [];
324
- }
325
-
326
- function selectSlashItem(idx) {
327
- if (idx < 0 || idx >= slashFiltered.length) return;
328
- var cmd = slashFiltered[idx];
329
- ctx.inputEl.value = "/" + cmd.name + " ";
330
- hideSlashMenu();
331
- autoResize();
332
- ctx.inputEl.focus();
333
- }
334
-
335
- function updateSlashHighlight() {
336
- ctx.slashMenu.querySelectorAll(".slash-item").forEach(function (el, i) {
337
- el.classList.toggle("active", i === slashActiveIdx);
338
- });
339
- var activeEl = ctx.slashMenu.querySelector(".slash-item.active");
340
- if (activeEl) activeEl.scrollIntoView({ block: "nearest" });
341
- }
342
-
343
- // --- Input sync across devices ---
344
- function sendInputSync() {
345
- if (isRemoteInput) return;
346
- if (ctx.ws && ctx.connected) {
347
- ctx.ws.send(JSON.stringify({ type: "input_sync", text: ctx.inputEl.value }));
348
- }
349
- }
350
-
351
- export function handleInputSync(text) {
352
- isRemoteInput = true;
353
- ctx.inputEl.value = text;
354
- autoResize();
355
- isRemoteInput = false;
356
- }
357
-
358
- // --- Attach menu ---
359
- var attachMenuOpen = false;
360
-
361
- function toggleAttachMenu() {
362
- var menu = document.getElementById("attach-menu");
363
- if (!menu) return;
364
- attachMenuOpen = !attachMenuOpen;
365
- menu.classList.toggle("hidden", !attachMenuOpen);
366
- }
367
-
368
- function closeAttachMenu() {
369
- var menu = document.getElementById("attach-menu");
370
- if (menu) menu.classList.add("hidden");
371
- attachMenuOpen = false;
372
- }
373
-
374
- function createFileInput(accept, capture, multiple) {
375
- var input = document.createElement("input");
376
- input.type = "file";
377
- input.accept = accept;
378
- if (capture) input.setAttribute("capture", capture);
379
- if (multiple) input.multiple = true;
380
- input.style.display = "none";
381
- document.body.appendChild(input);
382
-
383
- input.addEventListener("change", function () {
384
- if (input.files) {
385
- for (var i = 0; i < input.files.length; i++) {
386
- if (input.files[i].type.indexOf("image/") === 0) {
387
- readImageBlob(input.files[i]);
388
- }
389
- }
390
- }
391
- document.body.removeChild(input);
392
- });
393
-
394
- input.click();
395
- }
396
-
397
- // --- Init ---
398
- export function initInput(_ctx) {
399
- ctx = _ctx;
400
-
401
- // Attach button
402
- var isTouchDevice = "ontouchstart" in window;
403
- var attachBtn = document.getElementById("attach-btn");
404
- if (attachBtn) {
405
- attachBtn.addEventListener("click", function (e) {
406
- e.stopPropagation();
407
- // Desktop: skip menu, open file picker directly
408
- if (!isTouchDevice) {
409
- createFileInput("image/*", null, true);
410
- return;
411
- }
412
- toggleAttachMenu();
413
- });
414
- }
415
-
416
- var cameraBtn = document.getElementById("attach-camera");
417
- if (cameraBtn) {
418
- cameraBtn.addEventListener("click", function () {
419
- closeAttachMenu();
420
- createFileInput("image/*", "environment");
421
- });
422
- }
423
-
424
- var photosBtn = document.getElementById("attach-photos");
425
- if (photosBtn) {
426
- photosBtn.addEventListener("click", function () {
427
- closeAttachMenu();
428
- createFileInput("image/*", null, true);
429
- });
430
- }
431
-
432
- // Close attach menu when clicking outside
433
- document.addEventListener("click", function (e) {
434
- if (attachMenuOpen) {
435
- var wrap = document.getElementById("attach-wrap");
436
- if (wrap && !wrap.contains(e.target)) {
437
- closeAttachMenu();
438
- }
439
- }
440
- });
441
-
442
- // Paste handler
443
- document.addEventListener("paste", function (e) {
444
- var cd = e.clipboardData;
445
- if (!cd) return;
446
-
447
- var found = false;
448
-
449
- // Try clipboardData.files first (better Safari/iOS support)
450
- if (cd.files && cd.files.length > 0) {
451
- for (var i = 0; i < cd.files.length; i++) {
452
- if (cd.files[i].type.indexOf("image/") === 0) {
453
- found = true;
454
- readImageBlob(cd.files[i]);
455
- }
456
- }
457
- }
458
-
459
- // Fall back to clipboardData.items
460
- if (!found && cd.items) {
461
- for (var i = 0; i < cd.items.length; i++) {
462
- if (cd.items[i].type.indexOf("image/") === 0) {
463
- var blob = cd.items[i].getAsFile();
464
- if (blob) {
465
- found = true;
466
- readImageBlob(blob);
467
- }
468
- }
469
- }
470
- }
471
-
472
- // File path paste: detect file:// URIs or Finder file references
473
- if (!found) {
474
- var filePaths = extractFilePaths(cd);
475
- if (filePaths.length > 0) {
476
- e.preventDefault();
477
- insertTextAtCursor(filePaths.join("\n"));
478
- found = true;
479
- }
480
- }
481
-
482
- // Long text paste → pasted chip
483
- if (!found) {
484
- var pastedText = cd.getData("text/plain");
485
- if (pastedText && pastedText.length >= 500) {
486
- e.preventDefault();
487
- var preview = pastedText.substring(0, 50).replace(/\n/g, " ");
488
- if (pastedText.length > 50) preview += "...";
489
- pendingPastes.push({ text: pastedText, preview: preview });
490
- renderInputPreviews();
491
- found = true;
492
- }
493
- }
494
-
495
- if (found) e.preventDefault();
496
- });
497
-
498
- // Input event handlers
499
- ctx.inputEl.addEventListener("input", function () {
500
- autoResize();
501
- sendInputSync();
502
- var val = ctx.inputEl.value;
503
- if (val.startsWith("/") && !val.includes(" ") && val.length > 1) {
504
- showSlashMenu(val.substring(1));
505
- } else if (val === "/") {
506
- showSlashMenu("");
507
- } else {
508
- hideSlashMenu();
509
- }
510
- });
511
-
512
- ctx.inputEl.addEventListener("compositionstart", function () { isComposing = true; });
513
- ctx.inputEl.addEventListener("compositionend", function () { isComposing = false; });
514
-
515
- ctx.inputEl.addEventListener("keydown", function (e) {
516
- if (slashFiltered.length > 0 && ctx.slashMenu.classList.contains("visible")) {
517
- if (e.key === "ArrowDown") {
518
- e.preventDefault();
519
- slashActiveIdx = (slashActiveIdx + 1) % slashFiltered.length;
520
- updateSlashHighlight();
521
- return;
522
- }
523
- if (e.key === "ArrowUp") {
524
- e.preventDefault();
525
- slashActiveIdx = (slashActiveIdx - 1 + slashFiltered.length) % slashFiltered.length;
526
- updateSlashHighlight();
527
- return;
528
- }
529
- if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) {
530
- e.preventDefault();
531
- selectSlashItem(slashActiveIdx);
532
- return;
533
- }
534
- if (e.key === "Escape") {
535
- e.preventDefault();
536
- hideSlashMenu();
537
- return;
538
- }
539
- }
540
-
541
- // Ctrl+J: insert newline (like Claude CLI)
542
- if (e.key === "j" && e.ctrlKey && !e.metaKey) {
543
- e.preventDefault();
544
- var ta = ctx.inputEl;
545
- var start = ta.selectionStart;
546
- var end = ta.selectionEnd;
547
- var val = ta.value;
548
- ta.value = val.substring(0, start) + "\n" + val.substring(end);
549
- ta.selectionStart = ta.selectionEnd = start + 1;
550
- autoResize();
551
- return;
552
- }
553
-
554
- if (e.key === "Enter" && !e.shiftKey && !isComposing) {
555
- // Mobile: Enter inserts newline, send via button only
556
- if ("ontouchstart" in window) {
557
- return;
558
- }
559
- e.preventDefault();
560
- sendMessage();
561
- }
562
- });
563
-
564
- // Mobile: switch enterkeyhint to "enter" so keyboard shows return key
565
- if ("ontouchstart" in window) {
566
- ctx.inputEl.setAttribute("enterkeyhint", "enter");
567
- }
568
-
569
- // Send/Stop button
570
- ctx.sendBtn.addEventListener("click", function () {
571
- if (ctx.processing && ctx.connected) {
572
- ctx.ws.send(JSON.stringify({ type: "stop" }));
573
- return;
574
- }
575
- sendMessage();
576
- });
577
- ctx.sendBtn.addEventListener("dblclick", function (e) { e.preventDefault(); });
578
- }