openclacky 1.5.7 → 1.5.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/README.md +3 -3
  4. data/README_CN.md +2 -2
  5. data/README_JA.md +2 -2
  6. data/lib/clacky/agent.rb +2 -3
  7. data/lib/clacky/agent_config.rb +40 -18
  8. data/lib/clacky/brand_config.rb +12 -7
  9. data/lib/clacky/cli.rb +0 -1
  10. data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -1
  11. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +27 -14
  12. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +22 -5
  13. data/lib/clacky/default_extensions/general/ext.yml +1 -1
  14. data/lib/clacky/extension/api_loader.rb +4 -11
  15. data/lib/clacky/extension/packager.rb +1 -1
  16. data/lib/clacky/extension/scaffold/templates/hello/test/handler_test.rb.erb +22 -0
  17. data/lib/clacky/extension/verifier.rb +1 -1
  18. data/lib/clacky/media/generator.rb +3 -3
  19. data/lib/clacky/message_format/anthropic.rb +19 -2
  20. data/lib/clacky/message_format/bedrock.rb +8 -3
  21. data/lib/clacky/message_history.rb +1 -1
  22. data/lib/clacky/providers.rb +159 -105
  23. data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +6 -0
  24. data/lib/clacky/server/http_server.rb +67 -79
  25. data/lib/clacky/session_manager.rb +7 -0
  26. data/lib/clacky/skill_loader.rb +4 -0
  27. data/lib/clacky/tools/base.rb +11 -1
  28. data/lib/clacky/tools/edit.rb +28 -0
  29. data/lib/clacky/tools/terminal.rb +5 -0
  30. data/lib/clacky/tools/trash_manager.rb +1 -1
  31. data/lib/clacky/ui2/components/inline_input.rb +1 -0
  32. data/lib/clacky/ui2/components/input_area.rb +1 -0
  33. data/lib/clacky/ui2/components/modal_component.rb +0 -1
  34. data/lib/clacky/ui2/components/todo_area.rb +1 -0
  35. data/lib/clacky/ui2/line_editor.rb +1 -0
  36. data/lib/clacky/ui2/markdown_renderer.rb +5 -1
  37. data/lib/clacky/ui2/ui_controller.rb +1 -0
  38. data/lib/clacky/utils/model_pricing.rb +62 -27
  39. data/lib/clacky/utils/string_matcher.rb +29 -2
  40. data/lib/clacky/version.rb +1 -1
  41. data/lib/clacky/web/app.css +278 -221
  42. data/lib/clacky/web/components/chat-navigator.js +1 -1
  43. data/lib/clacky/web/features/billing/store.js +3 -3
  44. data/lib/clacky/web/features/billing/view.js +5 -61
  45. data/lib/clacky/web/features/extensions/store.js +65 -41
  46. data/lib/clacky/web/features/extensions/view.js +74 -10
  47. data/lib/clacky/web/features/new-session/view.js +24 -0
  48. data/lib/clacky/web/features/version/view.js +2 -2
  49. data/lib/clacky/web/i18n.js +36 -22
  50. data/lib/clacky/web/index.html +17 -17
  51. data/lib/clacky/web/projects.js +83 -285
  52. data/lib/clacky/web/sessions.js +151 -17
  53. data/lib/clacky/web/settings.js +6 -1
  54. metadata +2 -1
@@ -48,36 +48,47 @@ const Projects = (() => {
48
48
  }
49
49
 
50
50
  // ── Shared icon helper ────────────────────────────────────────────────────
51
- // Reads SVG path from the picker popup DOM and forces a consistent size.
52
- // Falls back to a hard-coded folder icon when the popup isn't in the DOM yet.
53
51
  const _FOLDER_PATH = `<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>`;
54
52
  const _FOLDER_OPEN_PATH = `<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/>`;
55
- function _getIconSvg(iconName, size = 14) {
56
- // folder-open is not in the picker; use the built-in path directly
53
+
54
+ const _ICON_PATHS = {
55
+ folder: _FOLDER_PATH,
56
+ code: `<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>`,
57
+ book: `<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>`,
58
+ globe: `<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>`,
59
+ zap: `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
60
+ star: `<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>`,
61
+ heart: `<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>`,
62
+ tool: `<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>`,
63
+ database: `<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>`,
64
+ image: `<rect x="3" y="3" width="16" height="16" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>`,
65
+ music: `<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>`,
66
+ coffee: `<path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/>`,
67
+ briefcase: `<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>`,
68
+ flask: `<path d="M9 3h6"/><path d="M10 3v6l-4 8a1 1 0 0 0 .9 1.5h10.2a1 1 0 0 0 .9-1.5L14 9V3"/>`,
69
+ chart: `<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>`,
70
+ rocket: `<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>`,
71
+ dollar: `<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>`,
72
+ cpu: `<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>`,
73
+ paint: `<circle cx="13.5" cy="6.5" r=".5"/><circle cx="17.5" cy="10.5" r=".5"/><circle cx="8.5" cy="7.5" r=".5"/><circle cx="6.5" cy="12.5" r=".5"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>`,
74
+ leaf: `<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/>`,
75
+ trophy: `<polyline points="8 21 8 14 4 9 4 4 20 4 20 9 16 14 16 21"/><line x1="8" y1="21" x2="16" y2="21"/><path d="M4 9H2a1 1 0 0 0-1 1 6 6 0 0 0 6 6"/><path d="M20 9h2a1 1 0 0 1 1 1 6 6 0 0 1-6 6"/>`,
76
+ smile: `<circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>`,
77
+ map: `<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/>`,
78
+ flower: `<path d="M12 7.5a4.5 4.5 0 1 1 4.5 4.5M12 7.5A4.5 4.5 0 1 0 7.5 12M12 7.5V9m-4.5 3a4.5 4.5 0 1 0 4.5 4.5M7.5 12H9m7.5 0a4.5 4.5 0 1 1-4.5 4.5m4.5-4.5H15m-3 4.5V15"/><circle cx="12" cy="12" r="3"/>`,
79
+ };
80
+
81
+ function _getIconSvg(iconName, size = 15.5) {
57
82
  if (iconName === "folder-open") {
58
83
  return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${_FOLDER_OPEN_PATH}</svg>`;
59
84
  }
60
- const popup = document.getElementById("project-picker-icons");
61
- let inner = null;
62
- if (popup) {
63
- const btn = popup.querySelector(`[data-icon="${iconName}"]`);
64
- if (btn) {
65
- // Clone the SVG and override size attrs
66
- const svg = btn.querySelector("svg");
67
- if (svg) inner = svg.innerHTML;
68
- }
69
- if (!inner) {
70
- const first = popup.querySelector("[data-icon] svg");
71
- if (first) inner = first.innerHTML;
72
- }
73
- }
74
- if (!inner) inner = _FOLDER_PATH;
85
+ const inner = _ICON_PATHS[iconName] || _FOLDER_PATH;
75
86
  return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
76
87
  }
77
88
 
78
89
  // ── State accessors ────────────────────────────────────────────────────────
79
90
 
80
- function all() { return _projects; }
91
+ function all() { return _sortProjects(_projects); }
81
92
 
82
93
  function find(id) { return id != null ? (_projects.find(p => p.id === String(id)) || null) : null; }
83
94
 
@@ -196,7 +207,9 @@ const Projects = (() => {
196
207
  const bi = sortedProjects.findIndex(p => p.id === b.project_id);
197
208
  return ai - bi;
198
209
  }
199
- // Secondary: within project, newest session first
210
+ // Secondary: within project, pinned first then newest
211
+ if (a.pinned && !b.pinned) return -1;
212
+ if (!a.pinned && b.pinned) return 1;
200
213
  const ta = a.updated_at || a.created_at || "";
201
214
  const tb = b.updated_at || b.created_at || "";
202
215
  return tb.localeCompare(ta);
@@ -230,7 +243,9 @@ const Projects = (() => {
230
243
  const sessions = Sessions.all
231
244
  .filter(s => s.project_id === project.id)
232
245
  .sort((a, b) => {
233
- // Sessions within a project: always newest first
246
+ // Pinned sessions first, then newest first
247
+ if (a.pinned && !b.pinned) return -1;
248
+ if (!a.pinned && b.pinned) return 1;
234
249
  const ta = a.updated_at || a.created_at || "";
235
250
  const tb = b.updated_at || b.created_at || "";
236
251
  return tb.localeCompare(ta);
@@ -271,29 +286,11 @@ const Projects = (() => {
271
286
  <path d="M12 8v8"/>
272
287
  <path d="M8 12h8"/>
273
288
  </svg>`;
274
- newSessionBtn.addEventListener("click", async e => {
289
+ newSessionBtn.addEventListener("click", e => {
275
290
  e.stopPropagation();
276
- try {
277
- const name = Sessions.nextDefaultName();
278
- const res = await fetch("/api/sessions", {
279
- method: "POST",
280
- headers: { "Content-Type": "application/json" },
281
- body: JSON.stringify({
282
- name,
283
- agent_profile: "general",
284
- source: "manual",
285
- project_id: project.id,
286
- }),
287
- });
288
- const data = await res.json();
289
- if (res.ok && data.session) {
290
- Sessions.add(data.session);
291
- renderSection();
292
- Sessions.select(data.session.id);
293
- }
294
- } catch (err) {
295
- console.error("Failed to create session:", err);
296
- }
291
+ const dir = project.working_dir || (NewSessionStore.state && NewSessionStore.state.defaultDir) || "";
292
+ NewSessionStore.updateAdvanced({ projectId: project.id, workingDir: dir });
293
+ Router.navigate("welcome");
297
294
  });
298
295
  // Actions button (⋯)
299
296
  const actionsBtn = document.createElement("button");
@@ -304,28 +301,13 @@ const Projects = (() => {
304
301
  </svg>`;
305
302
  actionsBtn.addEventListener("click", e => {
306
303
  e.stopPropagation();
307
- _clearTooltip();
308
304
  _showProjectMenu(project.id, actionsBtn);
309
305
  });
310
306
  header.appendChild(actionsBtn);
311
307
  header.appendChild(newSessionBtn);
312
308
 
313
- // Tooltip on hover — show after short delay; hide via shared _hideTimer
314
- header.addEventListener("mouseenter", () => {
315
- _cancelHide();
316
- clearTimeout(_tooltipTimer);
317
- _tooltipTimer = setTimeout(() => {
318
- _showProjectTooltip(project, sessions, header);
319
- }, 300);
320
- });
321
- header.addEventListener("mouseleave", () => {
322
- clearTimeout(_tooltipTimer);
323
- _scheduleHide();
324
- });
325
-
326
309
  // Toggle collapse on header click — in-place DOM toggle, no full re-render
327
310
  header.addEventListener("click", () => {
328
- _clearTooltip();
329
311
  const nowCollapsed = _collapsed.has(project.id);
330
312
  if (nowCollapsed) {
331
313
  _collapsed.delete(project.id);
@@ -372,156 +354,9 @@ const Projects = (() => {
372
354
  });
373
355
  }
374
356
 
375
- // ── Project tooltip ────────────────────────────────────────────────────────
376
-
377
- let _tooltipEl = null;
378
- let _tooltipTimer = null; // show delay
379
- let _hideTimer = null; // hide delay — shared by header + tooltip
380
-
381
- /** Start hide countdown. Cancelled if mouse re-enters header or tooltip. */
382
- function _scheduleHide() {
383
- clearTimeout(_hideTimer);
384
- _hideTimer = setTimeout(_clearTooltip, 150);
385
- }
386
-
387
- /** Cancel pending hide (mouse is back on header or tooltip). */
388
- function _cancelHide() {
389
- clearTimeout(_hideTimer);
390
- _hideTimer = null;
391
- }
392
-
393
- /** Create and position the hover tooltip for a project. */
394
- function _showProjectTooltip(project, sessions, anchorEl) {
395
- _clearTooltip();
396
-
397
- const tip = document.createElement("div");
398
- tip.className = "project-tooltip";
399
-
400
- // ── Header: icon + name + session count ──────────────────────────
401
- const header = document.createElement("div");
402
- header.className = "project-tooltip-header";
403
-
404
- // Project icon — use shared helper (12px, matches text size)
405
- const iconWrap = document.createElement("span");
406
- iconWrap.className = "project-tooltip-icon";
407
- iconWrap.innerHTML = _getIconSvg(project.icon || "folder", 12);
408
- if (project.color) iconWrap.style.color = project.color;
409
- header.appendChild(iconWrap);
410
-
411
- const nameEl = document.createElement("span");
412
- nameEl.className = "project-tooltip-name";
413
- nameEl.textContent = project.name;
414
- header.appendChild(nameEl);
415
-
416
- tip.appendChild(header);
417
-
418
- // ── Info rows ─────────────────────────────────────────────────────
419
- const rows = document.createElement("div");
420
- rows.className = "project-tooltip-rows";
421
-
422
- // Helper: build one row with icon (12px) + text
423
- const addRow = (iconSvgHtml, text, titleText) => {
424
- const row = document.createElement("div");
425
- row.className = "project-tooltip-row";
426
-
427
- const iconSpan = document.createElement("span");
428
- iconSpan.className = "project-tooltip-row-icon";
429
- iconSpan.innerHTML = iconSvgHtml;
430
- row.appendChild(iconSpan);
431
-
432
- const textSpan = document.createElement("span");
433
- textSpan.className = "project-tooltip-row-text";
434
- textSpan.textContent = text;
435
- if (titleText) textSpan.title = titleText;
436
- row.appendChild(textSpan);
437
-
438
- rows.appendChild(row);
439
- };
440
-
441
- // message-circle icon — simple chat bubble, no "+" (used for session count display)
442
- const chatIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg>`;
443
-
444
- // Sessions row
445
- const sessionText = sessions.length === 0
446
- ? I18n.t("projects.noSessions", "No sessions")
447
- : I18n.t("projects.tooltip.sessions", "{n} sessions").replace("{n}", sessions.length);
448
- addRow(chatIcon, sessionText);
449
-
450
- // Working dir row (only if set) — reuse picker folder icon at 12px
451
- if (project.working_dir) {
452
- const divider = document.createElement("div");
453
- divider.className = "project-tooltip-divider";
454
- rows.appendChild(divider);
455
-
456
- const dirRow = document.createElement("div");
457
- dirRow.className = "project-tooltip-row project-tooltip-row--clickable";
458
- dirRow.title = project.working_dir;
459
-
460
- const iconSpan = document.createElement("span");
461
- iconSpan.className = "project-tooltip-row-icon";
462
- iconSpan.innerHTML = _getIconSvg("folder", 12);
463
- dirRow.appendChild(iconSpan);
464
-
465
- const textSpan = document.createElement("span");
466
- textSpan.className = "project-tooltip-row-text";
467
- textSpan.textContent = project.working_dir;
468
- dirRow.appendChild(textSpan);
469
-
470
- // Click: reveal in Finder/Explorer
471
- dirRow.addEventListener("click", () => {
472
- fetch("/api/file-action", {
473
- method: "POST",
474
- headers: { "Content-Type": "application/json" },
475
- body: JSON.stringify({ path: project.working_dir, action: "reveal" }),
476
- }).catch(err => console.error("[Projects] reveal error:", err));
477
- _clearTooltip();
478
- });
479
-
480
- rows.appendChild(dirRow);
481
- }
482
-
483
- tip.appendChild(rows);
484
-
485
- // ── Positioning: right side of the sidebar ───────────────────────
486
- document.body.appendChild(tip);
487
- _tooltipEl = tip;
488
-
489
- // Hide when mouse leaves tooltip — use shared timer so gap crossing works
490
- tip.addEventListener("mouseenter", () => _cancelHide());
491
- tip.addEventListener("mouseleave", () => _scheduleHide());
492
-
493
- const rect = anchorEl.getBoundingClientRect();
494
- const tipRect = tip.getBoundingClientRect();
495
- const margin = 8; // gap between sidebar and tooltip card
496
-
497
- // Position tooltip 8px right of anchor; ::before pseudo fills the physical gap
498
- let left = rect.right + margin;
499
- if (left + tipRect.width > window.innerWidth - margin) {
500
- left = rect.left - tipRect.width - margin;
501
- }
502
-
503
- // Vertically align top with anchor row, but keep inside viewport
504
- let top = rect.top;
505
- if (top + tipRect.height > window.innerHeight - margin) {
506
- top = window.innerHeight - tipRect.height - margin;
507
- }
508
-
509
- tip.style.left = `${Math.max(margin, left)}px`;
510
- tip.style.top = `${Math.max(margin, top)}px`;
511
- }
512
-
513
- function _clearTooltip() {
514
- clearTimeout(_tooltipTimer);
515
- _tooltipTimer = null;
516
- clearTimeout(_hideTimer);
517
- _hideTimer = null;
518
- if (_tooltipEl) {
519
- _tooltipEl.remove();
520
- _tooltipEl = null;
521
- }
522
- }
357
+
523
358
 
524
- /** Show a small context menu for project actions (edit / delete). */
359
+ /** Show a small context menu for project actions. */
525
360
  function _showProjectMenu(projectId, anchor) {
526
361
  // Remove any existing project menu
527
362
  document.querySelectorAll(".project-actions-menu").forEach(m => m.remove());
@@ -529,10 +364,24 @@ const Projects = (() => {
529
364
  const menu = document.createElement("div");
530
365
  menu.className = "project-actions-menu";
531
366
 
532
- const addItem = (labelKey, defaultLabel, danger, onClick) => {
367
+ // Lucide icons, embedded to avoid extra asset requests.
368
+ const iconEdit = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4z"/></svg>`;
369
+ const iconClearSessions = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m16 22-1-4"/><path d="M19 14a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v1a1 1 0 0 0 1 1"/><path d="M19 14H5l-1.973 6.767A1 1 0 0 0 4 22h16a1 1 0 0 0 .973-1.233z"/><path d="m8 22 1-4"/></svg>`;
370
+ const iconTrash = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`;
371
+
372
+ const addItem = (labelKey, defaultLabel, iconSvg, danger, onClick) => {
533
373
  const item = document.createElement("button");
534
374
  item.className = "project-actions-menu-item" + (danger ? " danger" : "");
535
- item.textContent = I18n.t(labelKey, defaultLabel);
375
+
376
+ const icon = document.createElement("span");
377
+ icon.className = "project-actions-menu-icon";
378
+ icon.innerHTML = iconSvg;
379
+
380
+ const label = document.createElement("span");
381
+ label.className = "project-actions-menu-label";
382
+ label.textContent = I18n.t(labelKey, defaultLabel);
383
+
384
+ item.append(icon, label);
536
385
  item.addEventListener("click", e => {
537
386
  e.stopPropagation();
538
387
  menu.remove();
@@ -541,9 +390,9 @@ const Projects = (() => {
541
390
  menu.appendChild(item);
542
391
  };
543
392
 
544
- addItem("projects.menu.edit", "Edit", false, () => promptEdit(projectId));
545
- addItem("projects.menu.deleteSessions", "Delete Sessions", false, () => promptDeleteSessions(projectId));
546
- addItem("projects.menu.delete", "Delete", true, () => promptDelete(projectId));
393
+ addItem("projects.menu.edit", "Edit Project", iconEdit, false, () => promptEdit(projectId));
394
+ addItem("projects.menu.deleteSessions", "Delete Sessions", iconClearSessions, false, () => promptDeleteSessions(projectId));
395
+ addItem("projects.menu.delete", "Delete", iconTrash, true, () => promptDelete(projectId));
547
396
 
548
397
  // Position near anchor
549
398
  const rect = anchor.getBoundingClientRect();
@@ -647,6 +496,13 @@ const Projects = (() => {
647
496
  */
648
497
  function _ensureModalDOM() {
649
498
  if (document.getElementById("project-picker-popup")) return;
499
+
500
+ let iconsHTML = "";
501
+ for (const [name, path] of Object.entries(_ICON_PATHS)) {
502
+ const sel = name === "folder" ? " project-picker-icon-option--selected" : "";
503
+ iconsHTML += `<button class="project-picker-icon-option${sel}" data-icon="${name}" title="${name[0].toUpperCase()}${name.slice(1)}"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${path}</svg></button>`;
504
+ }
505
+
650
506
  const tpl = document.createElement("div");
651
507
  tpl.innerHTML = `
652
508
  <div id="project-picker-popup" class="project-picker-popup" style="display:none">
@@ -665,78 +521,7 @@ const Projects = (() => {
665
521
  <div class="project-picker-divider"></div>
666
522
  <!-- Icon grid -->
667
523
  <div class="project-picker-icons" id="project-picker-icons">
668
- <button class="project-picker-icon-option project-picker-icon-option--selected" data-icon="folder" title="Folder">
669
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
670
- </button>
671
- <button class="project-picker-icon-option" data-icon="code" title="Code">
672
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
673
- </button>
674
- <button class="project-picker-icon-option" data-icon="book" title="Book">
675
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
676
- </button>
677
- <button class="project-picker-icon-option" data-icon="globe" title="Globe">
678
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
679
- </button>
680
- <button class="project-picker-icon-option" data-icon="zap" title="Zap">
681
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
682
- </button>
683
- <button class="project-picker-icon-option" data-icon="star" title="Star">
684
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
685
- </button>
686
- <button class="project-picker-icon-option" data-icon="heart" title="Heart">
687
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
688
- </button>
689
- <button class="project-picker-icon-option" data-icon="tool" title="Tool">
690
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
691
- </button>
692
- <button class="project-picker-icon-option" data-icon="database" title="Database">
693
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
694
- </button>
695
- <button class="project-picker-icon-option" data-icon="image" title="Image">
696
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="16" height="16" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
697
- </button>
698
- <button class="project-picker-icon-option" data-icon="music" title="Music">
699
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
700
- </button>
701
- <button class="project-picker-icon-option" data-icon="coffee" title="Coffee">
702
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
703
- </button>
704
- <button class="project-picker-icon-option" data-icon="briefcase" title="Briefcase">
705
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
706
- </button>
707
- <button class="project-picker-icon-option" data-icon="flask" title="Flask">
708
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6"/><path d="M10 3v6l-4 8a1 1 0 0 0 .9 1.5h10.2a1 1 0 0 0 .9-1.5L14 9V3"/></svg>
709
- </button>
710
- <button class="project-picker-icon-option" data-icon="chart" title="Chart">
711
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
712
- </button>
713
- <button class="project-picker-icon-option" data-icon="rocket" title="Rocket">
714
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>
715
- </button>
716
- <button class="project-picker-icon-option" data-icon="dollar" title="Dollar">
717
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
718
- </button>
719
- <button class="project-picker-icon-option" data-icon="cpu" title="CPU">
720
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>
721
- </button>
722
- <button class="project-picker-icon-option" data-icon="paint" title="Paint">
723
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5"/><circle cx="17.5" cy="10.5" r=".5"/><circle cx="8.5" cy="7.5" r=".5"/><circle cx="6.5" cy="12.5" r=".5"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>
724
- </button>
725
- <button class="project-picker-icon-option" data-icon="leaf" title="Leaf">
726
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>
727
- </button>
728
- <button class="project-picker-icon-option" data-icon="trophy" title="Trophy">
729
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 21 8 14 4 9 4 4 20 4 20 9 16 14 16 21"/><line x1="8" y1="21" x2="16" y2="21"/><path d="M4 9H2a1 1 0 0 0-1 1 6 6 0 0 0 6 6"/><path d="M20 9h2a1 1 0 0 1 1 1 6 6 0 0 1-6 6"/></svg>
730
- </button>
731
- <button class="project-picker-icon-option" data-icon="smile" title="Smile">
732
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
733
- </button>
734
- <button class="project-picker-icon-option" data-icon="map" title="Map">
735
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
736
- </button>
737
- <button class="project-picker-icon-option" data-icon="flower" title="Flower">
738
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7.5a4.5 4.5 0 1 1 4.5 4.5M12 7.5A4.5 4.5 0 1 0 7.5 12M12 7.5V9m-4.5 3a4.5 4.5 0 1 0 4.5 4.5M7.5 12H9m7.5 0a4.5 4.5 0 1 1-4.5 4.5m4.5-4.5H15m-3 4.5V15"/><circle cx="12" cy="12" r="3"/></svg>
739
- </button>
524
+ ${iconsHTML}
740
525
  </div>
741
526
  <!-- Done button -->
742
527
  <div class="project-picker-footer">
@@ -775,6 +560,7 @@ const Projects = (() => {
775
560
  </button>
776
561
  </div>
777
562
  </div>
563
+ <div id="create-project-folder-hint" class="create-project-folder-hint" data-i18n="projects.create.folderDefaultHint">If not set, the default working directory ~/clacky_workspace will be used</div>
778
564
  </div>
779
565
  </div>
780
566
  <div class="modal-footer">
@@ -810,6 +596,7 @@ const Projects = (() => {
810
596
  const folderCard = $("create-project-folder-card");
811
597
  const folderName = $("create-project-folder-name");
812
598
  const folderRemoveBtn = $("create-project-folder-remove");
599
+ const folderHint = $("create-project-folder-hint");
813
600
 
814
601
  // State
815
602
  let selectedColor = ""; // "" = default (accent)
@@ -859,6 +646,7 @@ const Projects = (() => {
859
646
  folderCard.style.display = "none";
860
647
  folderArea.style.cursor = "pointer";
861
648
  }
649
+ folderHint.style.display = selectedDir ? "none" : "";
862
650
  };
863
651
 
864
652
  // Reset all state (prefill when editing an existing project)
@@ -1123,6 +911,12 @@ const Projects = (() => {
1123
911
  }
1124
912
  }
1125
913
 
914
+ function expand(projectId) {
915
+ if (!projectId) return;
916
+ _collapsed.delete(String(projectId));
917
+ _saveCollapsed();
918
+ }
919
+
1126
920
  // ── Init ───────────────────────────────────────────────────────────────────
1127
921
 
1128
922
  function init() {
@@ -1158,6 +952,8 @@ const Projects = (() => {
1158
952
  _showOrganizeMenu(btnOrganize);
1159
953
  });
1160
954
  }
955
+
956
+ document.addEventListener("langchange", () => renderSection());
1161
957
  }
1162
958
 
1163
959
  // ── Public API ─────────────────────────────────────────────────────────────
@@ -1174,6 +970,8 @@ const Projects = (() => {
1174
970
  promptEdit,
1175
971
  promptDelete,
1176
972
  renderSection,
973
+ expand,
974
+ getIconSvg: _getIconSvg,
1177
975
  init,
1178
976
  };
1179
977
  })();