clay-server 1.0.0-beta.1

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