openclacky 1.3.6 → 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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/lib/clacky/agent/hook_manager.rb +10 -1
  4. data/lib/clacky/agent/system_prompt_builder.rb +1 -1
  5. data/lib/clacky/agent.rb +17 -3
  6. data/lib/clacky/agent_profile.rb +124 -53
  7. data/lib/clacky/billing/billing_store.rb +3 -2
  8. data/lib/clacky/brand_config.rb +309 -6
  9. data/lib/clacky/cli.rb +4 -57
  10. data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
  11. data/lib/clacky/default_extensions/coding/ext.yml +19 -0
  12. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
  13. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  14. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  15. data/lib/clacky/default_extensions/ext-studio/ext.yml +33 -0
  16. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1288 -0
  17. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  18. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  19. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  20. data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
  21. data/lib/clacky/default_extensions/general/ext.yml +19 -0
  22. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +4 -5
  24. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  25. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  26. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -90
  27. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  28. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  29. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  30. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  31. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  32. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  33. data/lib/clacky/extension/api_loader.rb +136 -0
  34. data/lib/clacky/extension/cli_commands.rb +226 -0
  35. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  36. data/lib/clacky/extension/hook_loader.rb +77 -0
  37. data/lib/clacky/extension/loader.rb +489 -0
  38. data/lib/clacky/extension/packager.rb +226 -0
  39. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  40. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  41. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  42. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  43. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  44. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  45. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  46. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  47. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  48. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  49. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  50. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  51. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  52. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  53. data/lib/clacky/extension/scaffold.rb +55 -0
  54. data/lib/clacky/extension/verifier.rb +196 -0
  55. data/lib/clacky/identity.rb +70 -0
  56. data/lib/clacky/locales/en.rb +19 -1
  57. data/lib/clacky/locales/zh.rb +19 -1
  58. data/lib/clacky/platform_http_client.rb +18 -15
  59. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  60. data/lib/clacky/server/channel.rb +6 -0
  61. data/lib/clacky/server/http_server.rb +439 -146
  62. data/lib/clacky/server/session_registry.rb +9 -2
  63. data/lib/clacky/shell_hook_loader.rb +266 -22
  64. data/lib/clacky/skill_loader.rb +28 -1
  65. data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
  66. data/lib/clacky/ui2/ui_controller.rb +9 -6
  67. data/lib/clacky/utils/workspace_rules.rb +2 -2
  68. data/lib/clacky/version.rb +1 -1
  69. data/lib/clacky/web/app.css +1034 -675
  70. data/lib/clacky/web/app.js +63 -15
  71. data/lib/clacky/web/auth.js +2 -0
  72. data/lib/clacky/web/components/notify.js +2 -0
  73. data/lib/clacky/web/components/sidebar.js +1 -5
  74. data/lib/clacky/web/core/ext.js +432 -89
  75. data/lib/clacky/web/features/backup/store.js +1 -0
  76. data/lib/clacky/web/features/backup/view.js +0 -1
  77. data/lib/clacky/web/features/brand/view.js +13 -5
  78. data/lib/clacky/web/features/extensions/store.js +184 -0
  79. data/lib/clacky/web/features/extensions/view.js +390 -0
  80. data/lib/clacky/web/features/new-session/store.js +199 -0
  81. data/lib/clacky/web/features/new-session/view.js +619 -0
  82. data/lib/clacky/web/features/share/view.js +63 -14
  83. data/lib/clacky/web/features/skills/store.js +3 -1
  84. data/lib/clacky/web/features/skills/view.js +1 -1
  85. data/lib/clacky/web/features/tasks/view.js +12 -0
  86. data/lib/clacky/web/features/trash/store.js +24 -6
  87. data/lib/clacky/web/features/trash/view.js +48 -12
  88. data/lib/clacky/web/features/workspace/store.js +2 -0
  89. data/lib/clacky/web/features/workspace/view.js +0 -1
  90. data/lib/clacky/web/i18n.js +152 -106
  91. data/lib/clacky/web/index.html +172 -190
  92. data/lib/clacky/web/sessions.js +67 -272
  93. data/lib/clacky/web/settings.js +1 -1
  94. data/lib/clacky/web/skills.js +88 -48
  95. data/lib/clacky/web/theme.js +5 -0
  96. data/lib/clacky/web/ws-dispatcher.js +12 -2
  97. data/lib/clacky/web/ws.js +2 -0
  98. data/lib/clacky.rb +21 -10
  99. metadata +52 -21
  100. data/benchmark/runner.rb +0 -441
  101. data/lib/clacky/api_extension_loader.rb +0 -168
  102. data/lib/clacky/default_agents/SOUL.md +0 -3
  103. data/lib/clacky/default_agents/USER.md +0 -1
  104. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  105. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  106. data/lib/clacky/default_agents/general/profile.yml +0 -2
  107. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  108. data/lib/clacky/web/features/creator/store.js +0 -81
  109. data/lib/clacky/web/features/creator/view.js +0 -380
  110. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  111. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  112. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -0,0 +1,619 @@
1
+ // ── NewSession · view — landing page for #new ─────────────────────────────
2
+ //
3
+ // Renders the "start a new session" page: agent card grid, first-message
4
+ // composer, and the collapsible advanced-options section (name / model /
5
+ // working dir / init-project). Reads state from NewSessionStore and delegates
6
+ // I/O to it. On send, creates a session then hands off to Sessions.select()
7
+ // with a pending first message so the existing chat pipeline handles delivery.
8
+ //
9
+ // Depends on: NewSessionStore, Sessions, I18n, Router.
10
+ // ───────────────────────────────────────────────────────────────────────────
11
+
12
+ const NewSessionView = (() => {
13
+ const $ = (id) => document.getElementById(id);
14
+
15
+ let _initialized = false;
16
+ let _modelsLoaded = false;
17
+
18
+ function _isZh() {
19
+ return I18n.lang && I18n.lang().startsWith("zh");
20
+ }
21
+
22
+ function _agentLabel(a) {
23
+ if (_isZh() && a.title_zh) return a.title_zh;
24
+ return a.title || a.id;
25
+ }
26
+
27
+ function _agentDesc(a) {
28
+ if (_isZh() && a.description_zh) return a.description_zh;
29
+ return a.description || "";
30
+ }
31
+
32
+ const LS_SEEN_AGENTS_KEY = "openclacky.newSession.seenAgents";
33
+
34
+ function _seenAgents() {
35
+ try {
36
+ const raw = localStorage.getItem(LS_SEEN_AGENTS_KEY);
37
+ return raw ? JSON.parse(raw) : [];
38
+ } catch (e) {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function _markAgentSeen(id) {
44
+ if (!id) return;
45
+ const seen = _seenAgents();
46
+ if (seen.includes(id)) return;
47
+ seen.push(id);
48
+ try { localStorage.setItem(LS_SEEN_AGENTS_KEY, JSON.stringify(seen)); } catch (e) { /* ignore */ }
49
+ }
50
+
51
+ // Builtin agents (general/coding/ext-studio) ship with the app and are not
52
+ // "extensions" from the user's point of view — no EXT badge, never NEW.
53
+ function _isBuiltin(a) {
54
+ return a.layer === "builtin";
55
+ }
56
+
57
+ function _isNewAgent(a) {
58
+ if (_isBuiltin(a) || a.source === "user") return false;
59
+ return !_seenAgents().includes(a.id);
60
+ }
61
+
62
+ function _renderAgents() {
63
+ const container = $("new-session-agents");
64
+ if (!container) return;
65
+ const agents = NewSessionStore.state.agents;
66
+ const selected = NewSessionStore.state.selectedAgentId;
67
+
68
+ container.innerHTML = "";
69
+ agents.forEach((a) => {
70
+ const card = document.createElement("button");
71
+ card.type = "button";
72
+ card.className = "agent-card" + (a.id === selected ? " agent-card--selected" : "");
73
+ card.dataset.agentId = a.id;
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
+
92
+ const title = document.createElement("div");
93
+ title.className = "agent-card-title";
94
+ title.textContent = _agentLabel(a);
95
+ if (_isNewAgent(a)) {
96
+ const isNew = document.createElement("span");
97
+ isNew.className = "agent-card-new";
98
+ isNew.textContent = "NEW";
99
+ title.appendChild(isNew);
100
+ }
101
+ header.appendChild(title);
102
+ card.appendChild(header);
103
+
104
+ const descText = _agentDesc(a);
105
+ if (descText) {
106
+ const desc = document.createElement("div");
107
+ desc.className = "agent-card-desc";
108
+ desc.textContent = descText;
109
+ card.appendChild(desc);
110
+ }
111
+
112
+ const author = (a.author || "").trim();
113
+ if (author) {
114
+ const by = document.createElement("div");
115
+ by.className = "agent-card-author";
116
+ by.textContent = _isZh() ? `作者 ${author}` : `by ${author}`;
117
+ card.appendChild(by);
118
+ }
119
+
120
+ if (!_isBuiltin(a) && a.source === "extension") {
121
+ const badge = document.createElement("span");
122
+ badge.className = "agent-card-badge";
123
+ badge.textContent = "EXT";
124
+ card.appendChild(badge);
125
+ } else if (a.source === "user") {
126
+ const badge = document.createElement("span");
127
+ badge.className = "agent-card-badge agent-card-badge--custom";
128
+ badge.textContent = "custom";
129
+ card.appendChild(badge);
130
+ }
131
+
132
+ card.addEventListener("click", () => {
133
+ _markAgentSeen(a.id);
134
+ NewSessionStore.selectAgent(a.id);
135
+ _renderAgents();
136
+ });
137
+ container.appendChild(card);
138
+ });
139
+
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;
174
+ }
175
+
176
+ function _updatePlaceholder() {
177
+ const input = $("new-session-input");
178
+ if (!input) return;
179
+ input.placeholder = I18n.t("chat.input.placeholder");
180
+ }
181
+
182
+ async function _populateModels() {
183
+ if (_modelsLoaded) return;
184
+ const select = $("new-session-model");
185
+ if (!select) return;
186
+ const models = await NewSessionStore.loadModels();
187
+
188
+ select.innerHTML = "";
189
+ if (models.length === 0) {
190
+ const opt = document.createElement("option");
191
+ opt.value = "";
192
+ opt.textContent = "No models configured";
193
+ select.appendChild(opt);
194
+ _modelsLoaded = true;
195
+ return;
196
+ }
197
+
198
+ models.forEach((m) => {
199
+ const opt = document.createElement("option");
200
+ opt.value = m.id || "";
201
+ const typeBadge = m.type === "default" ? "[default] " : "";
202
+ opt.textContent = `${typeBadge}${m.model} (${m.api_key_masked})`;
203
+ if (m.type === "default") {
204
+ opt.selected = true;
205
+ NewSessionStore.updateAdvanced({ modelId: opt.value });
206
+ }
207
+ select.appendChild(opt);
208
+ });
209
+ _modelsLoaded = true;
210
+ }
211
+
212
+ async function _prefillDefaultDir() {
213
+ const dirInput = $("new-session-directory");
214
+ if (!dirInput || dirInput.value) return;
215
+ const home = await NewSessionStore.loadDefaultDirectory();
216
+ if (home && !dirInput.value) {
217
+ dirInput.value = home;
218
+ NewSessionStore.updateAdvanced({ workingDir: home });
219
+ }
220
+ }
221
+
222
+ function _updateInitProjectVisibility() {
223
+ const field = $("new-session-init-project-field");
224
+ if (!field) return;
225
+ const agent = NewSessionStore.currentAgent();
226
+ field.style.display = agent && agent.id === "coding" ? "block" : "none";
227
+ }
228
+
229
+ function _updateSendButton() {
230
+ const btn = $("new-session-send");
231
+ const input = $("new-session-input");
232
+ if (!btn || !input) return;
233
+ const hasText = input.value.trim().length > 0;
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;
434
+ }
435
+
436
+ async function _submit() {
437
+ const input = $("new-session-input");
438
+ if (!input) return;
439
+ const content = input.value.trim();
440
+ const hasFiles = _pendingImages.length > 0 || _pendingFiles.length > 0;
441
+ if (!content && !hasFiles) return;
442
+
443
+ const initCheckbox = $("new-session-init-project");
444
+ const initProject = initCheckbox && initCheckbox.checked;
445
+
446
+ const btn = $("new-session-send");
447
+ if (btn) btn.disabled = true;
448
+
449
+ const session = await NewSessionStore.createSession({
450
+ existingSessions: (Sessions && Sessions.all) || [],
451
+ });
452
+ if (!session) {
453
+ _updateSendButton();
454
+ return;
455
+ }
456
+
457
+ const files = hasFiles ? _buildPendingFiles() : null;
458
+ const display = hasFiles ? _buildBubbleHtml(content) : null;
459
+
460
+ Sessions.add(session);
461
+ Sessions.setPendingMessage(session.id, content, display, files);
462
+ input.value = "";
463
+ _pendingImages.length = 0;
464
+ _pendingFiles.length = 0;
465
+ _imageSeq = 0;
466
+ _renderAttachmentPreviews();
467
+ NewSessionStore.reset();
468
+
469
+ // Hand off to the normal chat pipeline: this switches the panel, wires
470
+ // up WS subscription, and the pending message is sent once subscribed
471
+ // (see ws-dispatcher.js "subscribed" branch).
472
+ Sessions.select(session.id);
473
+ }
474
+
475
+ function _bindOnce() {
476
+ if (_initialized) return;
477
+ _initialized = true;
478
+
479
+ NewSessionStore.on("newSession:agents-loaded", _renderAgents);
480
+ NewSessionStore.on("newSession:selection-changed", () => {
481
+ _renderAgents();
482
+ _updateInitProjectVisibility();
483
+ });
484
+ NewSessionStore.on("newSession:creating", _updateSendButton);
485
+
486
+ const input = $("new-session-input");
487
+ if (input) {
488
+ input.addEventListener("input", _updateSendButton);
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
+ }
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);
538
+ });
539
+ }
540
+
541
+ const sendBtn = $("new-session-send");
542
+ if (sendBtn) sendBtn.addEventListener("click", _submit);
543
+
544
+ const toggle = $("new-session-toggle-advanced");
545
+ const advanced = $("new-session-advanced");
546
+ if (toggle && advanced) {
547
+ toggle.addEventListener("click", async () => {
548
+ const open = advanced.hidden;
549
+ advanced.hidden = !open;
550
+ toggle.classList.toggle("is-open", open);
551
+ if (open) {
552
+ await _populateModels();
553
+ await _prefillDefaultDir();
554
+ requestAnimationFrame(() =>
555
+ advanced.scrollIntoView({ behavior: "smooth", block: "end" })
556
+ );
557
+ }
558
+ NewSessionStore.toggleAdvanced(open);
559
+ });
560
+ }
561
+
562
+ const nameInput = $("new-session-name");
563
+ if (nameInput) {
564
+ nameInput.addEventListener("input", (e) =>
565
+ NewSessionStore.updateAdvanced({ name: e.target.value })
566
+ );
567
+ }
568
+
569
+ const modelSelect = $("new-session-model");
570
+ if (modelSelect) {
571
+ modelSelect.addEventListener("change", (e) =>
572
+ NewSessionStore.updateAdvanced({ modelId: e.target.value })
573
+ );
574
+ }
575
+
576
+ const dirInput = $("new-session-directory");
577
+ if (dirInput) {
578
+ dirInput.addEventListener("input", (e) =>
579
+ NewSessionStore.updateAdvanced({ workingDir: e.target.value })
580
+ );
581
+ }
582
+
583
+ const browseBtn = $("new-session-browse-btn");
584
+ if (browseBtn && dirInput) {
585
+ browseBtn.addEventListener("click", async () => {
586
+ const start = dirInput.value.trim();
587
+ const picked = await window.openDirectoryPicker(start, null);
588
+ if (picked) {
589
+ dirInput.value = picked;
590
+ NewSessionStore.updateAdvanced({ workingDir: picked });
591
+ }
592
+ });
593
+ }
594
+
595
+ const initCheckbox = $("new-session-init-project");
596
+ if (initCheckbox) {
597
+ initCheckbox.addEventListener("change", (e) =>
598
+ NewSessionStore.updateAdvanced({ initProject: e.target.checked })
599
+ );
600
+ }
601
+ }
602
+
603
+ async function onPanelShow() {
604
+ _bindOnce();
605
+ if (NewSessionStore.state.agents.length === 0) {
606
+ await NewSessionStore.loadAgents();
607
+ } else {
608
+ _renderAgents();
609
+ }
610
+ _updateInitProjectVisibility();
611
+ _updateSendButton();
612
+ const input = $("new-session-input");
613
+ if (input) input.focus();
614
+ }
615
+
616
+ return { onPanelShow };
617
+ })();
618
+
619
+ window.NewSessionView = NewSessionView;