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