openclacky 1.5.7 → 1.5.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +2 -2
  4. data/README_CN.md +1 -1
  5. data/README_JA.md +1 -1
  6. data/lib/clacky/agent.rb +2 -3
  7. data/lib/clacky/agent_config.rb +40 -18
  8. data/lib/clacky/cli.rb +0 -1
  9. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +23 -10
  10. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +3 -2
  11. data/lib/clacky/default_extensions/general/ext.yml +1 -1
  12. data/lib/clacky/extension/api_loader.rb +4 -11
  13. data/lib/clacky/extension/packager.rb +1 -1
  14. data/lib/clacky/extension/verifier.rb +1 -1
  15. data/lib/clacky/media/generator.rb +3 -3
  16. data/lib/clacky/message_history.rb +1 -1
  17. data/lib/clacky/providers.rb +2 -1
  18. data/lib/clacky/server/http_server.rb +60 -76
  19. data/lib/clacky/session_manager.rb +7 -0
  20. data/lib/clacky/ui2/components/inline_input.rb +1 -0
  21. data/lib/clacky/ui2/components/input_area.rb +1 -0
  22. data/lib/clacky/ui2/components/modal_component.rb +0 -1
  23. data/lib/clacky/ui2/components/todo_area.rb +1 -0
  24. data/lib/clacky/ui2/line_editor.rb +1 -0
  25. data/lib/clacky/ui2/markdown_renderer.rb +5 -1
  26. data/lib/clacky/ui2/ui_controller.rb +1 -0
  27. data/lib/clacky/version.rb +1 -1
  28. data/lib/clacky/web/app.css +134 -210
  29. data/lib/clacky/web/components/chat-navigator.js +1 -1
  30. data/lib/clacky/web/features/billing/store.js +3 -3
  31. data/lib/clacky/web/features/billing/view.js +5 -61
  32. data/lib/clacky/web/features/extensions/store.js +1 -1
  33. data/lib/clacky/web/features/extensions/view.js +28 -5
  34. data/lib/clacky/web/features/new-session/view.js +24 -0
  35. data/lib/clacky/web/i18n.js +20 -18
  36. data/lib/clacky/web/index.html +12 -13
  37. data/lib/clacky/web/projects.js +73 -282
  38. data/lib/clacky/web/settings.js +6 -1
  39. metadata +1 -1
@@ -48,30 +48,41 @@ 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
 
@@ -271,29 +282,11 @@ const Projects = (() => {
271
282
  <path d="M12 8v8"/>
272
283
  <path d="M8 12h8"/>
273
284
  </svg>`;
274
- newSessionBtn.addEventListener("click", async e => {
285
+ newSessionBtn.addEventListener("click", e => {
275
286
  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
- }
287
+ const dir = project.working_dir || (NewSessionStore.state && NewSessionStore.state.defaultDir) || "";
288
+ NewSessionStore.updateAdvanced({ projectId: project.id, workingDir: dir });
289
+ Router.navigate("welcome");
297
290
  });
298
291
  // Actions button (⋯)
299
292
  const actionsBtn = document.createElement("button");
@@ -304,28 +297,13 @@ const Projects = (() => {
304
297
  </svg>`;
305
298
  actionsBtn.addEventListener("click", e => {
306
299
  e.stopPropagation();
307
- _clearTooltip();
308
300
  _showProjectMenu(project.id, actionsBtn);
309
301
  });
310
302
  header.appendChild(actionsBtn);
311
303
  header.appendChild(newSessionBtn);
312
304
 
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
305
  // Toggle collapse on header click — in-place DOM toggle, no full re-render
327
306
  header.addEventListener("click", () => {
328
- _clearTooltip();
329
307
  const nowCollapsed = _collapsed.has(project.id);
330
308
  if (nowCollapsed) {
331
309
  _collapsed.delete(project.id);
@@ -372,156 +350,9 @@ const Projects = (() => {
372
350
  });
373
351
  }
374
352
 
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
- };
353
+
440
354
 
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
- }
523
-
524
- /** Show a small context menu for project actions (edit / delete). */
355
+ /** Show a small context menu for project actions. */
525
356
  function _showProjectMenu(projectId, anchor) {
526
357
  // Remove any existing project menu
527
358
  document.querySelectorAll(".project-actions-menu").forEach(m => m.remove());
@@ -529,10 +360,24 @@ const Projects = (() => {
529
360
  const menu = document.createElement("div");
530
361
  menu.className = "project-actions-menu";
531
362
 
532
- const addItem = (labelKey, defaultLabel, danger, onClick) => {
363
+ // Lucide icons, embedded to avoid extra asset requests.
364
+ 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>`;
365
+ 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>`;
366
+ 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>`;
367
+
368
+ const addItem = (labelKey, defaultLabel, iconSvg, danger, onClick) => {
533
369
  const item = document.createElement("button");
534
370
  item.className = "project-actions-menu-item" + (danger ? " danger" : "");
535
- item.textContent = I18n.t(labelKey, defaultLabel);
371
+
372
+ const icon = document.createElement("span");
373
+ icon.className = "project-actions-menu-icon";
374
+ icon.innerHTML = iconSvg;
375
+
376
+ const label = document.createElement("span");
377
+ label.className = "project-actions-menu-label";
378
+ label.textContent = I18n.t(labelKey, defaultLabel);
379
+
380
+ item.append(icon, label);
536
381
  item.addEventListener("click", e => {
537
382
  e.stopPropagation();
538
383
  menu.remove();
@@ -541,9 +386,9 @@ const Projects = (() => {
541
386
  menu.appendChild(item);
542
387
  };
543
388
 
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));
389
+ addItem("projects.menu.edit", "Edit Project", iconEdit, false, () => promptEdit(projectId));
390
+ addItem("projects.menu.deleteSessions", "Delete Sessions", iconClearSessions, false, () => promptDeleteSessions(projectId));
391
+ addItem("projects.menu.delete", "Delete", iconTrash, true, () => promptDelete(projectId));
547
392
 
548
393
  // Position near anchor
549
394
  const rect = anchor.getBoundingClientRect();
@@ -647,6 +492,13 @@ const Projects = (() => {
647
492
  */
648
493
  function _ensureModalDOM() {
649
494
  if (document.getElementById("project-picker-popup")) return;
495
+
496
+ let iconsHTML = "";
497
+ for (const [name, path] of Object.entries(_ICON_PATHS)) {
498
+ const sel = name === "folder" ? " project-picker-icon-option--selected" : "";
499
+ 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>`;
500
+ }
501
+
650
502
  const tpl = document.createElement("div");
651
503
  tpl.innerHTML = `
652
504
  <div id="project-picker-popup" class="project-picker-popup" style="display:none">
@@ -665,78 +517,7 @@ const Projects = (() => {
665
517
  <div class="project-picker-divider"></div>
666
518
  <!-- Icon grid -->
667
519
  <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>
520
+ ${iconsHTML}
740
521
  </div>
741
522
  <!-- Done button -->
742
523
  <div class="project-picker-footer">
@@ -775,6 +556,7 @@ const Projects = (() => {
775
556
  </button>
776
557
  </div>
777
558
  </div>
559
+ <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
560
  </div>
779
561
  </div>
780
562
  <div class="modal-footer">
@@ -810,6 +592,7 @@ const Projects = (() => {
810
592
  const folderCard = $("create-project-folder-card");
811
593
  const folderName = $("create-project-folder-name");
812
594
  const folderRemoveBtn = $("create-project-folder-remove");
595
+ const folderHint = $("create-project-folder-hint");
813
596
 
814
597
  // State
815
598
  let selectedColor = ""; // "" = default (accent)
@@ -859,6 +642,7 @@ const Projects = (() => {
859
642
  folderCard.style.display = "none";
860
643
  folderArea.style.cursor = "pointer";
861
644
  }
645
+ folderHint.style.display = selectedDir ? "none" : "";
862
646
  };
863
647
 
864
648
  // Reset all state (prefill when editing an existing project)
@@ -1123,6 +907,12 @@ const Projects = (() => {
1123
907
  }
1124
908
  }
1125
909
 
910
+ function expand(projectId) {
911
+ if (!projectId) return;
912
+ _collapsed.delete(String(projectId));
913
+ _saveCollapsed();
914
+ }
915
+
1126
916
  // ── Init ───────────────────────────────────────────────────────────────────
1127
917
 
1128
918
  function init() {
@@ -1174,6 +964,7 @@ const Projects = (() => {
1174
964
  promptEdit,
1175
965
  promptDelete,
1176
966
  renderSection,
967
+ expand,
1177
968
  init,
1178
969
  };
1179
970
  })();
@@ -2337,7 +2337,12 @@ const Settings = (() => {
2337
2337
 
2338
2338
  if (source === "custom") {
2339
2339
  if (cur.source !== "custom" && !_mediaCustomDraft[kind]) {
2340
- _mediaCustomDraft[kind] = { model: "", base_url: "", api_key: "" };
2340
+ const sc = cur.saved_custom;
2341
+ _mediaCustomDraft[kind] = {
2342
+ model: (sc && sc.model) || "",
2343
+ base_url: (sc && sc.base_url) || "",
2344
+ api_key: cur.api_key_masked || ""
2345
+ };
2341
2346
  }
2342
2347
  _mediaState[kind] = { ...cur, source: "custom" };
2343
2348
  _renderMediaRows();
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.7
4
+ version: 1.5.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy