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.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/bin/claude-relay.js +6 -0
- package/bin/cli.js +2602 -0
- package/lib/cli-sessions.js +265 -0
- package/lib/config.js +338 -0
- package/lib/daemon.js +802 -0
- package/lib/ipc.js +124 -0
- package/lib/notes.js +121 -0
- package/lib/pages.js +1308 -0
- package/lib/project.js +3172 -0
- package/lib/public/app.js +4795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/admin.css +576 -0
- package/lib/public/css/base.css +284 -0
- package/lib/public/css/diff.css +139 -0
- package/lib/public/css/filebrowser.css +1482 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +614 -0
- package/lib/public/css/input.css +654 -0
- package/lib/public/css/loop.css +898 -0
- package/lib/public/css/menus.css +823 -0
- package/lib/public/css/messages.css +1448 -0
- package/lib/public/css/mobile-nav.css +384 -0
- package/lib/public/css/overlays.css +893 -0
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/rewind.css +528 -0
- package/lib/public/css/scheduler-modal.css +1429 -0
- package/lib/public/css/scheduler.css +1306 -0
- package/lib/public/css/server-settings.css +811 -0
- package/lib/public/css/sidebar.css +1189 -0
- package/lib/public/css/skills.css +789 -0
- package/lib/public/css/sticky-notes.css +848 -0
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +517 -0
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -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-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +1437 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/admin.js +631 -0
- package/lib/public/modules/ascii-logo.js +442 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1535 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +661 -0
- package/lib/public/modules/markdown.js +378 -0
- package/lib/public/modules/notifications.js +548 -0
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +378 -0
- package/lib/public/modules/project-settings.js +901 -0
- package/lib/public/modules/qrcode.js +67 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/scheduler.js +2833 -0
- package/lib/public/modules/server-settings.js +928 -0
- package/lib/public/modules/sidebar.js +2264 -0
- package/lib/public/modules/skills.js +794 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +1253 -0
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +736 -0
- package/lib/public/modules/theme.js +720 -0
- package/lib/public/modules/tools.js +1622 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +24 -0
- package/lib/public/sw.js +154 -0
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/push.js +130 -0
- package/lib/scheduler.js +402 -0
- package/lib/sdk-bridge.js +1035 -0
- package/lib/server.js +2055 -0
- package/lib/sessions.js +552 -0
- package/lib/smtp.js +221 -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/lib/users.js +459 -0
- package/lib/utils.js +64 -0
- 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
|
+
}
|