openclacky 1.3.7 → 1.3.9
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/lib/clacky/agent/hook_manager.rb +10 -1
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent.rb +9 -2
- data/lib/clacky/agent_profile.rb +7 -4
- data/lib/clacky/billing/billing_store.rb +3 -2
- data/lib/clacky/brand_config.rb +2 -1
- data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
- data/lib/clacky/default_extensions/coding/ext.yml +4 -4
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +0 -1
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -3
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +118 -50
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +1 -2
- data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
- data/lib/clacky/default_extensions/general/ext.yml +4 -4
- data/lib/clacky/default_extensions/git/ext.yml +0 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +3 -3
- data/lib/clacky/default_extensions/meeting/ext.yml +0 -1
- data/lib/clacky/default_extensions/meeting/panels/meeting/view.js +1 -2
- data/lib/clacky/default_extensions/time_machine/ext.yml +0 -1
- data/lib/clacky/extension/api_extension.rb +11 -2
- data/lib/clacky/extension/dispatcher.rb +3 -2
- data/lib/clacky/extension/loader.rb +6 -0
- data/lib/clacky/extension/packager.rb +0 -6
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/server/http_server.rb +149 -5
- data/lib/clacky/shell_hook_loader.rb +266 -22
- data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
- data/lib/clacky/utils/workspace_rules.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +334 -73
- data/lib/clacky/web/core/ext.js +130 -24
- data/lib/clacky/web/features/extensions/store.js +41 -6
- data/lib/clacky/web/features/extensions/view.js +44 -13
- data/lib/clacky/web/features/new-session/store.js +13 -0
- data/lib/clacky/web/features/new-session/view.js +314 -14
- data/lib/clacky/web/features/trash/view.js +4 -5
- data/lib/clacky/web/i18n.js +44 -2
- data/lib/clacky/web/index.html +54 -16
- data/lib/clacky/web/sessions.js +3 -3
- data/lib/clacky/web/skills.js +86 -48
- data/lib/clacky/web/theme.js +3 -0
- data/lib/clacky/web/ws-dispatcher.js +11 -2
- metadata +7 -7
|
@@ -72,6 +72,23 @@ const NewSessionView = (() => {
|
|
|
72
72
|
card.className = "agent-card" + (a.id === selected ? " agent-card--selected" : "");
|
|
73
73
|
card.dataset.agentId = a.id;
|
|
74
74
|
|
|
75
|
+
const header = document.createElement("div");
|
|
76
|
+
header.className = "agent-card-header";
|
|
77
|
+
|
|
78
|
+
const avatar = document.createElement("div");
|
|
79
|
+
avatar.className = "agent-card-avatar";
|
|
80
|
+
if (a.avatar) {
|
|
81
|
+
const img = document.createElement("img");
|
|
82
|
+
img.src = a.avatar;
|
|
83
|
+
img.alt = "";
|
|
84
|
+
img.loading = "lazy";
|
|
85
|
+
avatar.appendChild(img);
|
|
86
|
+
} else {
|
|
87
|
+
avatar.classList.add("agent-card-avatar--fallback");
|
|
88
|
+
avatar.textContent = (_agentLabel(a) || "?").trim().charAt(0).toUpperCase();
|
|
89
|
+
}
|
|
90
|
+
header.appendChild(avatar);
|
|
91
|
+
|
|
75
92
|
const title = document.createElement("div");
|
|
76
93
|
title.className = "agent-card-title";
|
|
77
94
|
title.textContent = _agentLabel(a);
|
|
@@ -81,7 +98,8 @@ const NewSessionView = (() => {
|
|
|
81
98
|
isNew.textContent = "NEW";
|
|
82
99
|
title.appendChild(isNew);
|
|
83
100
|
}
|
|
84
|
-
|
|
101
|
+
header.appendChild(title);
|
|
102
|
+
card.appendChild(header);
|
|
85
103
|
|
|
86
104
|
const descText = _agentDesc(a);
|
|
87
105
|
if (descText) {
|
|
@@ -120,17 +138,45 @@ const NewSessionView = (() => {
|
|
|
120
138
|
});
|
|
121
139
|
|
|
122
140
|
_updatePlaceholder();
|
|
141
|
+
_renderAgentPanels();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _renderAgentPanels() {
|
|
145
|
+
const box = $("new-session-agent-panels");
|
|
146
|
+
if (!box) return;
|
|
147
|
+
const agent = NewSessionStore.currentAgent();
|
|
148
|
+
const ext = window.Clacky && Clacky.ext;
|
|
149
|
+
const contrib = (agent && ext && typeof ext.contributionsForAgent === "function")
|
|
150
|
+
? ext.contributionsForAgent(agent.id)
|
|
151
|
+
: null;
|
|
152
|
+
const panels = (contrib && contrib.panels) || [];
|
|
153
|
+
const skills = (contrib && contrib.skills) || [];
|
|
154
|
+
if (!panels.length && !skills.length) {
|
|
155
|
+
box.style.display = "none";
|
|
156
|
+
box.replaceChildren();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const zh = _isZh();
|
|
160
|
+
const label = document.createElement("span");
|
|
161
|
+
label.className = "ns-panels-label";
|
|
162
|
+
label.textContent = zh ? "此助手额外提供" : "This agent adds";
|
|
163
|
+
box.replaceChildren(label);
|
|
164
|
+
panels.forEach((p) => box.appendChild(_contribChip((zh && p.title_zh) ? p.title_zh : p.title, "panel")));
|
|
165
|
+
skills.forEach((s) => box.appendChild(_contribChip((zh && s.title_zh) ? s.title_zh : s.title, "skill")));
|
|
166
|
+
box.style.display = "flex";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _contribChip(text, kind) {
|
|
170
|
+
const chip = document.createElement("span");
|
|
171
|
+
chip.className = `ns-panel-chip ns-panel-chip--${kind}`;
|
|
172
|
+
chip.textContent = text;
|
|
173
|
+
return chip;
|
|
123
174
|
}
|
|
124
175
|
|
|
125
176
|
function _updatePlaceholder() {
|
|
126
177
|
const input = $("new-session-input");
|
|
127
178
|
if (!input) return;
|
|
128
|
-
|
|
129
|
-
const label = agent ? _agentLabel(agent) : "";
|
|
130
|
-
const tpl = I18n.t("sessions.new.placeholder");
|
|
131
|
-
input.placeholder = label
|
|
132
|
-
? tpl.replace("{{agent}}", label)
|
|
133
|
-
: tpl.replace("{{agent}}", "");
|
|
179
|
+
input.placeholder = I18n.t("chat.input.placeholder");
|
|
134
180
|
}
|
|
135
181
|
|
|
136
182
|
async function _populateModels() {
|
|
@@ -185,14 +231,214 @@ const NewSessionView = (() => {
|
|
|
185
231
|
const input = $("new-session-input");
|
|
186
232
|
if (!btn || !input) return;
|
|
187
233
|
const hasText = input.value.trim().length > 0;
|
|
188
|
-
|
|
234
|
+
const hasFiles = _pendingImages.length > 0 || _pendingFiles.length > 0;
|
|
235
|
+
btn.disabled = (!hasText && !hasFiles) || NewSessionStore.state.creating;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Attachments (image compression + generic file upload) ───────────────
|
|
239
|
+
// A trimmed mirror of the chat composer's pipeline (sessions.js). New-session
|
|
240
|
+
// has no live session, so attachments are staged here and handed to the chat
|
|
241
|
+
// pipeline as a pending message once the session is created & subscribed.
|
|
242
|
+
const _pendingImages = [];
|
|
243
|
+
const _pendingFiles = [];
|
|
244
|
+
let _imageSeq = 0;
|
|
245
|
+
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
246
|
+
const MAX_IMAGE_BYTES_SEND = 512 * 1024;
|
|
247
|
+
const MAX_IMAGE_LONG_EDGE = 1920;
|
|
248
|
+
const MAX_FILE_BYTES = 32 * 1024 * 1024;
|
|
249
|
+
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
250
|
+
|
|
251
|
+
function _docTypeIcon(mimeType, filename) {
|
|
252
|
+
const lower = (filename || "").toLowerCase();
|
|
253
|
+
if (mimeType === "application/pdf" || lower.endsWith(".pdf")) return "📄";
|
|
254
|
+
if (mimeType === "application/zip" || lower.endsWith(".zip")) return "🗜️";
|
|
255
|
+
if (lower.endsWith(".tar") || lower.endsWith(".gz") || lower.endsWith(".tgz") ||
|
|
256
|
+
lower.endsWith(".tar.gz") || lower.endsWith(".rar") || lower.endsWith(".7z")) return "🗜️";
|
|
257
|
+
if ((mimeType && mimeType.includes("wordprocessingml")) || lower.endsWith(".doc") || lower.endsWith(".docx")) return "📝";
|
|
258
|
+
if ((mimeType && mimeType.includes("spreadsheetml")) || lower.endsWith(".xls") || lower.endsWith(".xlsx")) return "📊";
|
|
259
|
+
if ((mimeType && mimeType.includes("presentationml")) || lower.endsWith(".ppt") || lower.endsWith(".pptx")) return "📋";
|
|
260
|
+
if (mimeType === "text/csv" || lower.endsWith(".csv")) return "📊";
|
|
261
|
+
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "📝";
|
|
262
|
+
return "📎";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _compressImage(file) {
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const reader = new FileReader();
|
|
268
|
+
reader.onerror = () => reject(new Error("Failed to read image"));
|
|
269
|
+
reader.onload = (e) => {
|
|
270
|
+
const img = new Image();
|
|
271
|
+
img.onerror = () => reject(new Error("Failed to decode image"));
|
|
272
|
+
img.onload = () => {
|
|
273
|
+
let { width, height } = img;
|
|
274
|
+
if (width > MAX_IMAGE_LONG_EDGE || height > MAX_IMAGE_LONG_EDGE) {
|
|
275
|
+
const ratio = Math.min(MAX_IMAGE_LONG_EDGE / width, MAX_IMAGE_LONG_EDGE / height);
|
|
276
|
+
width = Math.round(width * ratio);
|
|
277
|
+
height = Math.round(height * ratio);
|
|
278
|
+
}
|
|
279
|
+
const canvas = document.createElement("canvas");
|
|
280
|
+
canvas.width = width;
|
|
281
|
+
canvas.height = height;
|
|
282
|
+
const ctx = canvas.getContext("2d");
|
|
283
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
284
|
+
const isPNG = file.type === "image/png";
|
|
285
|
+
if (isPNG) {
|
|
286
|
+
let dataUrl = canvas.toDataURL("image/png");
|
|
287
|
+
let scale = 0.9;
|
|
288
|
+
while (dataUrl.length * 0.75 > MAX_IMAGE_BYTES_SEND && scale > 0.3) {
|
|
289
|
+
const sw = Math.round(width * scale);
|
|
290
|
+
const sh = Math.round(height * scale);
|
|
291
|
+
canvas.width = sw;
|
|
292
|
+
canvas.height = sh;
|
|
293
|
+
ctx.drawImage(img, 0, 0, sw, sh);
|
|
294
|
+
dataUrl = canvas.toDataURL("image/png");
|
|
295
|
+
scale -= 0.1;
|
|
296
|
+
}
|
|
297
|
+
resolve(dataUrl);
|
|
298
|
+
} else {
|
|
299
|
+
let quality = 0.85;
|
|
300
|
+
let dataUrl = canvas.toDataURL("image/jpeg", quality);
|
|
301
|
+
while (dataUrl.length * 0.75 > MAX_IMAGE_BYTES_SEND && quality > 0.2) {
|
|
302
|
+
quality -= 0.1;
|
|
303
|
+
dataUrl = canvas.toDataURL("image/jpeg", quality);
|
|
304
|
+
}
|
|
305
|
+
resolve(dataUrl);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
img.src = e.target.result;
|
|
309
|
+
};
|
|
310
|
+
reader.readAsDataURL(file);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function _addImageFile(file) {
|
|
315
|
+
if (file.size > MAX_IMAGE_SIZE) {
|
|
316
|
+
alert(I18n.t("chat.file.imageTooLarge", { name: file.name, max: "5 MB" }));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const seq = ++_imageSeq;
|
|
320
|
+
const ext = (file.name.split(".").pop() || "png").toLowerCase();
|
|
321
|
+
const displayName = `IMG_${String(seq).padStart(3, "0")}.${ext}`;
|
|
322
|
+
_compressImage(file)
|
|
323
|
+
.then((dataUrl) => {
|
|
324
|
+
_pendingImages.push({ dataUrl, name: displayName, mimeType: file.type === "image/png" ? "image/png" : "image/jpeg", seq });
|
|
325
|
+
_renderAttachmentPreviews();
|
|
326
|
+
})
|
|
327
|
+
.catch((err) => alert(I18n.t("chat.file.processFailed", { msg: err.message })));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function _addGenericFile(file) {
|
|
331
|
+
if (file.size > MAX_FILE_BYTES) {
|
|
332
|
+
alert(I18n.t("chat.file.tooLarge", { name: file.name, max: "32 MB" }));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const formData = new FormData();
|
|
336
|
+
formData.append("file", file);
|
|
337
|
+
fetch("/api/upload", { method: "POST", body: formData })
|
|
338
|
+
.then((r) => r.json())
|
|
339
|
+
.then((data) => {
|
|
340
|
+
if (!data.ok) { alert(I18n.t("chat.file.uploadFailed", { msg: data.error })); return; }
|
|
341
|
+
_pendingFiles.push({ name: data.name, path: data.path, mime_type: file.type });
|
|
342
|
+
_renderAttachmentPreviews();
|
|
343
|
+
})
|
|
344
|
+
.catch((err) => alert(`Upload error: ${err.message}`));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function _addAttachmentFile(file) {
|
|
348
|
+
if (file && ACCEPTED_IMAGE_TYPES.includes(file.type)) _addImageFile(file);
|
|
349
|
+
else _addGenericFile(file);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function _renderAttachmentPreviews() {
|
|
353
|
+
const strip = $("ns-image-preview-strip");
|
|
354
|
+
if (!strip) return;
|
|
355
|
+
strip.innerHTML = "";
|
|
356
|
+
const hasContent = _pendingImages.length > 0 || _pendingFiles.length > 0;
|
|
357
|
+
strip.style.display = hasContent ? "flex" : "none";
|
|
358
|
+
_updateSendButton();
|
|
359
|
+
if (!hasContent) return;
|
|
360
|
+
|
|
361
|
+
_pendingImages.forEach((img, idx) => {
|
|
362
|
+
const item = document.createElement("div");
|
|
363
|
+
item.className = "img-preview-item";
|
|
364
|
+
item.title = img.name;
|
|
365
|
+
const thumbnail = document.createElement("img");
|
|
366
|
+
thumbnail.src = img.dataUrl;
|
|
367
|
+
thumbnail.alt = img.name;
|
|
368
|
+
const removeBtn = document.createElement("button");
|
|
369
|
+
removeBtn.className = "img-preview-remove";
|
|
370
|
+
removeBtn.textContent = "✕";
|
|
371
|
+
removeBtn.title = "Remove";
|
|
372
|
+
removeBtn.addEventListener("click", () => { _pendingImages.splice(idx, 1); _renderAttachmentPreviews(); });
|
|
373
|
+
item.appendChild(thumbnail);
|
|
374
|
+
item.appendChild(removeBtn);
|
|
375
|
+
strip.appendChild(item);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
_pendingFiles.forEach((f, idx) => {
|
|
379
|
+
const item = document.createElement("div");
|
|
380
|
+
item.className = "pdf-preview-item";
|
|
381
|
+
item.title = f.name;
|
|
382
|
+
const icon = document.createElement("div");
|
|
383
|
+
icon.className = "pdf-preview-icon";
|
|
384
|
+
icon.textContent = _docTypeIcon(f.mime_type, f.name);
|
|
385
|
+
const info = document.createElement("div");
|
|
386
|
+
info.className = "pdf-preview-info";
|
|
387
|
+
const name = document.createElement("div");
|
|
388
|
+
name.className = "pdf-preview-name";
|
|
389
|
+
name.textContent = f.name;
|
|
390
|
+
const typeLabel = document.createElement("div");
|
|
391
|
+
typeLabel.className = "pdf-preview-type";
|
|
392
|
+
typeLabel.textContent = (f.name.split(".").pop() || "file").toUpperCase();
|
|
393
|
+
info.appendChild(name);
|
|
394
|
+
info.appendChild(typeLabel);
|
|
395
|
+
const removeBtn = document.createElement("button");
|
|
396
|
+
removeBtn.className = "pdf-preview-remove";
|
|
397
|
+
removeBtn.textContent = "✕";
|
|
398
|
+
removeBtn.title = "Remove";
|
|
399
|
+
removeBtn.addEventListener("click", () => { _pendingFiles.splice(idx, 1); _renderAttachmentPreviews(); });
|
|
400
|
+
item.appendChild(icon);
|
|
401
|
+
item.appendChild(info);
|
|
402
|
+
item.appendChild(removeBtn);
|
|
403
|
+
strip.appendChild(item);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _buildPendingFiles() {
|
|
408
|
+
_pendingImages.sort((a, b) => a.seq - b.seq);
|
|
409
|
+
return [
|
|
410
|
+
..._pendingImages.map((img) => ({ name: img.name, mime_type: img.mimeType || "image/jpeg", data_url: img.dataUrl })),
|
|
411
|
+
..._pendingFiles.map((f) => ({ name: f.name, path: f.path, mime_type: f.mime_type })),
|
|
412
|
+
];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function _buildBubbleHtml(content) {
|
|
416
|
+
let html = content ? escapeHtml(content) : "";
|
|
417
|
+
if (_pendingImages.length > 0) {
|
|
418
|
+
const thumbs = _pendingImages
|
|
419
|
+
.map((img) => `<img src="${img.dataUrl}" alt="${escapeHtml(img.name)}" class="msg-image-thumb">`)
|
|
420
|
+
.join("");
|
|
421
|
+
html = thumbs + (html ? "<br>" + html : "");
|
|
422
|
+
}
|
|
423
|
+
if (_pendingFiles.length > 0) {
|
|
424
|
+
const badges = _pendingFiles.map((f) => {
|
|
425
|
+
const icon = _docTypeIcon(f.mime_type, f.name);
|
|
426
|
+
const ext = (f.name.split(".").pop() || "file").toUpperCase();
|
|
427
|
+
return `<span class="msg-pdf-badge"><span class="msg-pdf-badge-icon">${icon}</span>` +
|
|
428
|
+
`<span class="msg-pdf-badge-info"><span class="msg-pdf-badge-name">${escapeHtml(f.name)}</span>` +
|
|
429
|
+
`<span class="msg-pdf-badge-type">${escapeHtml(ext)}</span></span></span>`;
|
|
430
|
+
}).join(" ");
|
|
431
|
+
html = badges + (html ? "<br>" + html : "");
|
|
432
|
+
}
|
|
433
|
+
return html;
|
|
189
434
|
}
|
|
190
435
|
|
|
191
436
|
async function _submit() {
|
|
192
437
|
const input = $("new-session-input");
|
|
193
438
|
if (!input) return;
|
|
194
439
|
const content = input.value.trim();
|
|
195
|
-
|
|
440
|
+
const hasFiles = _pendingImages.length > 0 || _pendingFiles.length > 0;
|
|
441
|
+
if (!content && !hasFiles) return;
|
|
196
442
|
|
|
197
443
|
const initCheckbox = $("new-session-init-project");
|
|
198
444
|
const initProject = initCheckbox && initCheckbox.checked;
|
|
@@ -208,9 +454,16 @@ const NewSessionView = (() => {
|
|
|
208
454
|
return;
|
|
209
455
|
}
|
|
210
456
|
|
|
457
|
+
const files = hasFiles ? _buildPendingFiles() : null;
|
|
458
|
+
const display = hasFiles ? _buildBubbleHtml(content) : null;
|
|
459
|
+
|
|
211
460
|
Sessions.add(session);
|
|
212
|
-
Sessions.setPendingMessage(session.id, content);
|
|
461
|
+
Sessions.setPendingMessage(session.id, content, display, files);
|
|
213
462
|
input.value = "";
|
|
463
|
+
_pendingImages.length = 0;
|
|
464
|
+
_pendingFiles.length = 0;
|
|
465
|
+
_imageSeq = 0;
|
|
466
|
+
_renderAttachmentPreviews();
|
|
214
467
|
NewSessionStore.reset();
|
|
215
468
|
|
|
216
469
|
// Hand off to the normal chat pipeline: this switches the panel, wires
|
|
@@ -233,11 +486,55 @@ const NewSessionView = (() => {
|
|
|
233
486
|
const input = $("new-session-input");
|
|
234
487
|
if (input) {
|
|
235
488
|
input.addEventListener("input", _updateSendButton);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
489
|
+
// Enter-to-send + slash-command navigation are owned by SkillAC.attach().
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Slash-command skill autocomplete for this composer (mirrors chat).
|
|
493
|
+
if (typeof SkillAC !== "undefined" && input) {
|
|
494
|
+
SkillAC.attach({
|
|
495
|
+
input: "new-session-input",
|
|
496
|
+
dropdown: "ns-skill-autocomplete",
|
|
497
|
+
list: "ns-skill-autocomplete-list",
|
|
498
|
+
slashBtn: "ns-btn-slash",
|
|
499
|
+
systemChk: "ns-chk-ac-show-system-skills",
|
|
500
|
+
fetchSkills: async () => {
|
|
501
|
+
const agent = NewSessionStore.currentAgent();
|
|
502
|
+
if (!agent) return [];
|
|
503
|
+
return NewSessionStore.fetchSkillsForAgent(agent.id);
|
|
504
|
+
},
|
|
505
|
+
onSend: _submit,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Attachments: attach button → file picker, drag-drop, paste.
|
|
510
|
+
const fileInput = $("ns-file-input");
|
|
511
|
+
const attachBtn = $("ns-btn-attach");
|
|
512
|
+
if (attachBtn && fileInput) {
|
|
513
|
+
attachBtn.addEventListener("click", () => fileInput.click());
|
|
514
|
+
fileInput.addEventListener("change", (e) => {
|
|
515
|
+
Array.from(e.target.files).forEach(_addAttachmentFile);
|
|
516
|
+
e.target.value = "";
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (input) {
|
|
520
|
+
input.addEventListener("paste", (e) => {
|
|
521
|
+
const items = (e.clipboardData && e.clipboardData.items) || [];
|
|
522
|
+
let handled = false;
|
|
523
|
+
for (const it of items) {
|
|
524
|
+
if (it.kind === "file") {
|
|
525
|
+
const file = it.getAsFile();
|
|
526
|
+
if (file) { _addAttachmentFile(file); handled = true; }
|
|
527
|
+
}
|
|
240
528
|
}
|
|
529
|
+
if (handled) e.preventDefault();
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const composer = document.querySelector("#welcome .new-session-composer");
|
|
533
|
+
if (composer) {
|
|
534
|
+
composer.addEventListener("dragover", (e) => { e.preventDefault(); });
|
|
535
|
+
composer.addEventListener("drop", (e) => {
|
|
536
|
+
e.preventDefault();
|
|
537
|
+
Array.from(e.dataTransfer.files || []).forEach(_addAttachmentFile);
|
|
241
538
|
});
|
|
242
539
|
}
|
|
243
540
|
|
|
@@ -254,6 +551,9 @@ const NewSessionView = (() => {
|
|
|
254
551
|
if (open) {
|
|
255
552
|
await _populateModels();
|
|
256
553
|
await _prefillDefaultDir();
|
|
554
|
+
requestAnimationFrame(() =>
|
|
555
|
+
advanced.scrollIntoView({ behavior: "smooth", block: "end" })
|
|
556
|
+
);
|
|
257
557
|
}
|
|
258
558
|
NewSessionStore.toggleAdvanced(open);
|
|
259
559
|
});
|
|
@@ -77,12 +77,8 @@ const TrashView = (() => {
|
|
|
77
77
|
|
|
78
78
|
function _load() {
|
|
79
79
|
if (_activeTab === "file-trash") {
|
|
80
|
-
const list = $("trash-list");
|
|
81
|
-
if (list) list.innerHTML = `<div class="trash-loading">${_t("trash.loading")}</div>`;
|
|
82
80
|
Trash.loadFiles();
|
|
83
81
|
} else {
|
|
84
|
-
const list = $("trash-session-list");
|
|
85
|
-
if (list) list.innerHTML = `<div class="creator-loading">${_t("trash.loading")}</div>`;
|
|
86
82
|
Trash.loadSessions();
|
|
87
83
|
}
|
|
88
84
|
}
|
|
@@ -182,7 +178,8 @@ const TrashView = (() => {
|
|
|
182
178
|
// ── Session trash rendering ──────────────────────────────────────────
|
|
183
179
|
|
|
184
180
|
function _renderSkeleton() {
|
|
185
|
-
const
|
|
181
|
+
const isSession = _activeTab === "session-trash";
|
|
182
|
+
const list = $(isSession ? "trash-session-list" : "trash-list");
|
|
186
183
|
if (!list) return;
|
|
187
184
|
let html = "";
|
|
188
185
|
for (let i = 0; i < 3; i++) {
|
|
@@ -425,6 +422,8 @@ const TrashView = (() => {
|
|
|
425
422
|
}
|
|
426
423
|
|
|
427
424
|
function _subscribe() {
|
|
425
|
+
Trash.on("trash:filesLoading", _renderSkeleton);
|
|
426
|
+
Trash.on("trash:sessionsLoading", _renderSkeleton);
|
|
428
427
|
Trash.on("trash:filesChanged", _renderFiles);
|
|
429
428
|
Trash.on("trash:sessionsChanged", _renderSessions);
|
|
430
429
|
Trash.on("trash:filesError", (e) => {
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -136,6 +136,23 @@ const I18n = (() => {
|
|
|
136
136
|
"workspace.revealInFinder": "Reveal in Finder",
|
|
137
137
|
"workspace.revealFailed": "Failed to reveal file",
|
|
138
138
|
"changes.tab": "Git",
|
|
139
|
+
"changes.loading": "Loading changes…",
|
|
140
|
+
"changes.error": "Failed to load changes",
|
|
141
|
+
"changes.noRepo": "This project has no version control enabled.",
|
|
142
|
+
"changes.cleanTitle": "No changes",
|
|
143
|
+
"changes.changedPre": "AI changed ",
|
|
144
|
+
"changes.filesUnit": "file(s)",
|
|
145
|
+
"changes.sub": "Managed by Git · since last snapshot",
|
|
146
|
+
"changes.branchPre": "Current branch: ",
|
|
147
|
+
"changes.clean": "Working tree is clean — no unsaved changes.",
|
|
148
|
+
"changes.tag.add": "Added",
|
|
149
|
+
"changes.tag.mod": "Modified",
|
|
150
|
+
"changes.tag.del": "Deleted",
|
|
151
|
+
"changes.save.prefix": "Manual snapshot",
|
|
152
|
+
"changes.save.btn": "Save snapshot",
|
|
153
|
+
"changes.save.saving": "Saving…",
|
|
154
|
+
"changes.save.done": "Saved — restore it from History",
|
|
155
|
+
"changes.save.failed": "Save failed",
|
|
139
156
|
"tm.tab": "History",
|
|
140
157
|
"sib.model.tooltip": "Click to switch model",
|
|
141
158
|
"sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
|
|
@@ -496,6 +513,8 @@ const I18n = (() => {
|
|
|
496
513
|
"extensions.sort.newest": "Newest",
|
|
497
514
|
"extensions.sort.updated": "Recently updated",
|
|
498
515
|
"extensions.sort.downloads": "Most downloaded",
|
|
516
|
+
"extensions.filter.all": "All",
|
|
517
|
+
"extensions.filter.installed": "Installed",
|
|
499
518
|
"extensions.empty": "No extensions available yet.",
|
|
500
519
|
"extensions.noResults": "No extensions match your search.",
|
|
501
520
|
"extensions.loadFailed": "Could not reach the extension store.",
|
|
@@ -517,10 +536,12 @@ const I18n = (() => {
|
|
|
517
536
|
"extensions.section.panels": "Panels",
|
|
518
537
|
"extensions.section.api": "APIs",
|
|
519
538
|
"extensions.disabled": "Disabled",
|
|
539
|
+
"extensions.action.install": "Install",
|
|
540
|
+
"extensions.action.installing": "Installing…",
|
|
520
541
|
"extensions.action.disable": "Disable",
|
|
521
542
|
"extensions.action.enable": "Enable",
|
|
522
543
|
"extensions.action.remove": "Remove",
|
|
523
|
-
"extensions.action.removeConfirm": "Remove this extension? Its files will be deleted. You can reinstall it
|
|
544
|
+
"extensions.action.removeConfirm": "Remove this extension? Its files will be deleted. You can reinstall it later.",
|
|
524
545
|
"mcp.loading": "Loading…",
|
|
525
546
|
"mcp.empty.title": "No MCP servers configured",
|
|
526
547
|
"mcp.empty.body": "Create ~/.clacky/mcp.json to plug in MCP servers. The format matches Claude Desktop and Cursor, so existing configs work as-is.",
|
|
@@ -1077,6 +1098,23 @@ const I18n = (() => {
|
|
|
1077
1098
|
"workspace.revealInFinder": "打开所在文件夹",
|
|
1078
1099
|
"workspace.revealFailed": "无法打开文件位置",
|
|
1079
1100
|
"changes.tab": "Git 管理",
|
|
1101
|
+
"changes.loading": "正在读取改动…",
|
|
1102
|
+
"changes.error": "读取改动失败",
|
|
1103
|
+
"changes.noRepo": "这个项目还没有启用版本管理。",
|
|
1104
|
+
"changes.cleanTitle": "暂无改动",
|
|
1105
|
+
"changes.changedPre": "AI 改了 ",
|
|
1106
|
+
"changes.filesUnit": "个文件",
|
|
1107
|
+
"changes.sub": "由 Git 管理 · 自上次存档以来",
|
|
1108
|
+
"changes.branchPre": "当前分支:",
|
|
1109
|
+
"changes.clean": "工作区是干净的,没有未存档的改动。",
|
|
1110
|
+
"changes.tag.add": "新增",
|
|
1111
|
+
"changes.tag.mod": "修改",
|
|
1112
|
+
"changes.tag.del": "删除",
|
|
1113
|
+
"changes.save.prefix": "手动存档",
|
|
1114
|
+
"changes.save.btn": "存档当前版本",
|
|
1115
|
+
"changes.save.saving": "正在存档…",
|
|
1116
|
+
"changes.save.done": "已存档,可在「时光机」里回到这个版本",
|
|
1117
|
+
"changes.save.failed": "存档失败",
|
|
1080
1118
|
"tm.tab": "时光机",
|
|
1081
1119
|
"sib.model.tooltip": "点击切换模型",
|
|
1082
1120
|
"sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
|
|
@@ -1436,6 +1474,8 @@ const I18n = (() => {
|
|
|
1436
1474
|
"extensions.sort.newest": "最新",
|
|
1437
1475
|
"extensions.sort.updated": "最近更新",
|
|
1438
1476
|
"extensions.sort.downloads": "下载最多",
|
|
1477
|
+
"extensions.filter.all": "全部",
|
|
1478
|
+
"extensions.filter.installed": "已安装",
|
|
1439
1479
|
"extensions.empty": "暂无可用扩展。",
|
|
1440
1480
|
"extensions.noResults": "没有匹配的扩展。",
|
|
1441
1481
|
"extensions.loadFailed": "无法连接扩展市场。",
|
|
@@ -1457,10 +1497,12 @@ const I18n = (() => {
|
|
|
1457
1497
|
"extensions.section.panels": "面板",
|
|
1458
1498
|
"extensions.section.api": "API",
|
|
1459
1499
|
"extensions.disabled": "已禁用",
|
|
1500
|
+
"extensions.action.install": "安装",
|
|
1501
|
+
"extensions.action.installing": "安装中…",
|
|
1460
1502
|
"extensions.action.disable": "禁用",
|
|
1461
1503
|
"extensions.action.enable": "启用",
|
|
1462
1504
|
"extensions.action.remove": "移除",
|
|
1463
|
-
"extensions.action.removeConfirm": "
|
|
1505
|
+
"extensions.action.removeConfirm": "确定移除此扩展?相关文件会被删除,之后可重新安装。",
|
|
1464
1506
|
"mcp.loading": "加载中…",
|
|
1465
1507
|
"mcp.empty.title": "尚未配置 MCP 服务",
|
|
1466
1508
|
"mcp.empty.body": "新建 ~/.clacky/mcp.json 即可接入 MCP 服务。格式与 Claude Desktop、Cursor 一致,现有配置可直接复用。",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -311,11 +311,15 @@
|
|
|
311
311
|
<!-- New Session page (default landing view; hash: #new or empty) -->
|
|
312
312
|
<div id="welcome" class="centered new-session-page">
|
|
313
313
|
<div class="new-session-inner">
|
|
314
|
+
<div class="new-session-body">
|
|
314
315
|
<h2 class="new-session-title" data-i18n="sessions.new.title">Start a new session</h2>
|
|
315
316
|
<p class="new-session-subtitle" data-i18n="sessions.new.subtitle">Pick who you want to chat with, then type your first message.</p>
|
|
316
317
|
|
|
317
318
|
<div id="new-session-agents" class="new-session-agents"></div>
|
|
318
319
|
|
|
320
|
+
<!-- Panels the selected agent exposes (filled when an agent is picked) -->
|
|
321
|
+
<div id="new-session-agent-panels" class="new-session-agent-panels" style="display:none"></div>
|
|
322
|
+
|
|
319
323
|
<div class="new-session-advanced-wrap">
|
|
320
324
|
<button id="new-session-toggle-advanced" class="new-session-advanced-toggle" type="button" aria-expanded="false">
|
|
321
325
|
<span data-i18n="sessions.new.moreOptions">More options</span>
|
|
@@ -356,16 +360,43 @@
|
|
|
356
360
|
</div>
|
|
357
361
|
</div>
|
|
358
362
|
</div>
|
|
363
|
+
</div><!-- /.new-session-body -->
|
|
359
364
|
|
|
360
365
|
<div class="new-session-composer">
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
366
|
+
<!-- Skill autocomplete dropdown (shown when user types /xxx) -->
|
|
367
|
+
<div id="ns-skill-autocomplete" class="skill-autocomplete" style="display:none" role="listbox" aria-label="Skills">
|
|
368
|
+
<div class="skill-ac-header-row">
|
|
369
|
+
<div class="skill-ac-left">
|
|
370
|
+
<div class="skill-ac-title" data-i18n="sidebar.skills">Skills</div>
|
|
371
|
+
<label class="skill-ac-filter-toggle">
|
|
372
|
+
<input type="checkbox" id="ns-chk-ac-show-system-skills">
|
|
373
|
+
<span class="skill-ac-toggle-track"></span>
|
|
374
|
+
<span class="skill-ac-filter-label" data-i18n="skills.filter.showSystemShort">System</span>
|
|
375
|
+
</label>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div id="ns-skill-autocomplete-list"></div>
|
|
379
|
+
</div>
|
|
380
|
+
<!-- Attachment preview strip (hidden when empty) -->
|
|
381
|
+
<div id="ns-image-preview-strip" style="display:none"></div>
|
|
382
|
+
<div id="ns-input-bar" class="new-session-input-bar">
|
|
383
|
+
<input type="file" id="ns-file-input" accept="*/*" multiple style="display:none">
|
|
384
|
+
<button id="ns-btn-attach" type="button" class="new-session-composer-btn"
|
|
385
|
+
title="Attach file (image, pdf, docx, md, tar.gz…) — drag & drop / Ctrl+V also work">
|
|
386
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
387
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.41 17.41a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
|
|
388
|
+
</svg>
|
|
389
|
+
</button>
|
|
390
|
+
<button id="ns-btn-slash" type="button" class="new-session-composer-btn" title="Insert skill command (/)">
|
|
391
|
+
<span>/</span>
|
|
392
|
+
</button>
|
|
393
|
+
<textarea id="new-session-input"
|
|
394
|
+
class="new-session-textarea"
|
|
395
|
+
rows="1"
|
|
396
|
+
data-i18n-placeholder="chat.input.placeholder"
|
|
397
|
+
placeholder="Message… (Enter to send, Shift+Enter for newline)"></textarea>
|
|
398
|
+
<button id="new-session-send" class="new-session-send" type="button" title="Send message" disabled>
|
|
399
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
|
369
400
|
</button>
|
|
370
401
|
</div>
|
|
371
402
|
</div>
|
|
@@ -404,6 +435,8 @@
|
|
|
404
435
|
<div id="new-message-banner" class="new-message-banner" style="display:none">
|
|
405
436
|
<span data-i18n="chat.newMessageHint">New messages ↓</span>
|
|
406
437
|
</div>
|
|
438
|
+
<!-- Composer slot: agent-scoped controls above the session info bar. -->
|
|
439
|
+
<div id="ext-slot-session-composer" class="session-composer" data-slot="session.composer"></div>
|
|
407
440
|
<!-- Session info bar — mirrors CLI bottom status bar -->
|
|
408
441
|
<div id="session-info-bar" style="display:none">
|
|
409
442
|
<!-- Order: status | id | dir | model | mode | tasks | cost -->
|
|
@@ -448,8 +481,7 @@
|
|
|
448
481
|
</span>
|
|
449
482
|
</div>
|
|
450
483
|
|
|
451
|
-
<!-- Composer slot:
|
|
452
|
-
<div id="ext-slot-session-composer" class="session-composer" data-slot="session.composer"></div>
|
|
484
|
+
<!-- Composer slot: moved above the session info bar. -->
|
|
453
485
|
<div id="input-area">
|
|
454
486
|
<div id="ws-disconnect-hint" style="display:none"></div>
|
|
455
487
|
<!-- Skill autocomplete dropdown (shown when user types /xxx) -->
|
|
@@ -630,12 +662,18 @@
|
|
|
630
662
|
</div>
|
|
631
663
|
|
|
632
664
|
<div class="extensions-toolbar">
|
|
633
|
-
<
|
|
634
|
-
|
|
635
|
-
<
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
665
|
+
<div class="extensions-filter-tabs">
|
|
666
|
+
<button type="button" class="extensions-filter-tab extensions-filter-tab-active" data-filter="all" data-i18n="extensions.filter.all">All</button>
|
|
667
|
+
<button type="button" class="extensions-filter-tab" data-filter="installed" data-i18n="extensions.filter.installed">Installed</button>
|
|
668
|
+
</div>
|
|
669
|
+
<div class="extensions-toolbar-right">
|
|
670
|
+
<input type="text" id="extensions-search-input" class="extensions-search" placeholder="Search extensions…" />
|
|
671
|
+
<select id="extensions-sort" class="extensions-sort">
|
|
672
|
+
<option value="newest" data-i18n="extensions.sort.newest">Newest</option>
|
|
673
|
+
<option value="updated" data-i18n="extensions.sort.updated">Recently updated</option>
|
|
674
|
+
<option value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</option>
|
|
675
|
+
</select>
|
|
676
|
+
</div>
|
|
639
677
|
</div>
|
|
640
678
|
|
|
641
679
|
<div id="extensions-warning" class="brand-skills-warning" style="display:none"></div>
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -3989,9 +3989,9 @@ const Sessions = (() => {
|
|
|
3989
3989
|
return id;
|
|
3990
3990
|
},
|
|
3991
3991
|
|
|
3992
|
-
/** Register a
|
|
3993
|
-
setPendingMessage(sessionId, content, display = null) {
|
|
3994
|
-
_pendingMessage = { session_id: sessionId, content, display };
|
|
3992
|
+
/** Register a message (optionally with attachments) to send after subscribe is confirmed. */
|
|
3993
|
+
setPendingMessage(sessionId, content, display = null, files = null) {
|
|
3994
|
+
_pendingMessage = { session_id: sessionId, content, display, files };
|
|
3995
3995
|
},
|
|
3996
3996
|
|
|
3997
3997
|
/** Consume and return the pending message (clears it). */
|