openclacky 1.3.7 → 1.3.8
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 +24 -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/default_extensions/coding/agents/coding/avatar.png +0 -0
- data/lib/clacky/default_extensions/coding/ext.yml +4 -3
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +24 -5
- data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
- data/lib/clacky/default_extensions/general/ext.yml +4 -3
- data/lib/clacky/default_extensions/git/panels/git/view.js +3 -3
- data/lib/clacky/default_extensions/meeting/panels/meeting/view.js +1 -2
- data/lib/clacky/extension/loader.rb +6 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/server/http_server.rb +129 -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 +230 -46
- data/lib/clacky/web/core/ext.js +130 -24
- data/lib/clacky/web/features/extensions/view.js +4 -3
- 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 +34 -0
- data/lib/clacky/web/index.html +43 -11
- 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
|
@@ -168,6 +168,18 @@ const NewSessionStore = (() => {
|
|
|
168
168
|
_emit("newSession:reset", {});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
async function fetchSkillsForAgent(agentId) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/skills`);
|
|
174
|
+
if (!res.ok) return [];
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
return data.skills || [];
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error("Failed to load skills:", e);
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
return {
|
|
172
184
|
get state() { return _state; },
|
|
173
185
|
on: _on,
|
|
@@ -179,6 +191,7 @@ const NewSessionStore = (() => {
|
|
|
179
191
|
loadModels,
|
|
180
192
|
loadDefaultDirectory,
|
|
181
193
|
createSession,
|
|
194
|
+
fetchSkillsForAgent,
|
|
182
195
|
reset,
|
|
183
196
|
};
|
|
184
197
|
})();
|
|
@@ -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",
|
|
@@ -1077,6 +1094,23 @@ const I18n = (() => {
|
|
|
1077
1094
|
"workspace.revealInFinder": "打开所在文件夹",
|
|
1078
1095
|
"workspace.revealFailed": "无法打开文件位置",
|
|
1079
1096
|
"changes.tab": "Git 管理",
|
|
1097
|
+
"changes.loading": "正在读取改动…",
|
|
1098
|
+
"changes.error": "读取改动失败",
|
|
1099
|
+
"changes.noRepo": "这个项目还没有启用版本管理。",
|
|
1100
|
+
"changes.cleanTitle": "暂无改动",
|
|
1101
|
+
"changes.changedPre": "AI 改了 ",
|
|
1102
|
+
"changes.filesUnit": "个文件",
|
|
1103
|
+
"changes.sub": "由 Git 管理 · 自上次存档以来",
|
|
1104
|
+
"changes.branchPre": "当前分支:",
|
|
1105
|
+
"changes.clean": "工作区是干净的,没有未存档的改动。",
|
|
1106
|
+
"changes.tag.add": "新增",
|
|
1107
|
+
"changes.tag.mod": "修改",
|
|
1108
|
+
"changes.tag.del": "删除",
|
|
1109
|
+
"changes.save.prefix": "手动存档",
|
|
1110
|
+
"changes.save.btn": "存档当前版本",
|
|
1111
|
+
"changes.save.saving": "正在存档…",
|
|
1112
|
+
"changes.save.done": "已存档,可在「时光机」里回到这个版本",
|
|
1113
|
+
"changes.save.failed": "存档失败",
|
|
1080
1114
|
"tm.tab": "时光机",
|
|
1081
1115
|
"sib.model.tooltip": "点击切换模型",
|
|
1082
1116
|
"sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
|
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,12 @@
|
|
|
630
662
|
</div>
|
|
631
663
|
|
|
632
664
|
<div class="extensions-toolbar">
|
|
633
|
-
<input type="text" id="extensions-search-input" class="extensions-search" placeholder="Search extensions…" />
|
|
634
665
|
<select id="extensions-sort" class="extensions-sort">
|
|
635
666
|
<option value="newest" data-i18n="extensions.sort.newest">Newest</option>
|
|
636
667
|
<option value="updated" data-i18n="extensions.sort.updated">Recently updated</option>
|
|
637
668
|
<option value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</option>
|
|
638
669
|
</select>
|
|
670
|
+
<input type="text" id="extensions-search-input" class="extensions-search" placeholder="Search extensions…" />
|
|
639
671
|
</div>
|
|
640
672
|
|
|
641
673
|
<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). */
|