openclacky 1.2.17 → 1.3.0

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 +34 -0
  3. data/lib/clacky/agent/skill_manager.rb +1 -1
  4. data/lib/clacky/agent/time_machine.rb +256 -74
  5. data/lib/clacky/agent/tool_executor.rb +12 -0
  6. data/lib/clacky/agent.rb +21 -31
  7. data/lib/clacky/agent_config.rb +18 -0
  8. data/lib/clacky/cli.rb +55 -3
  9. data/lib/clacky/default_skills/media-gen/SKILL.md +173 -5
  10. data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -0
  11. data/lib/clacky/media/base.rb +125 -0
  12. data/lib/clacky/media/dashscope.rb +243 -0
  13. data/lib/clacky/media/gemini.rb +10 -0
  14. data/lib/clacky/media/generator.rb +75 -0
  15. data/lib/clacky/media/openai_compat.rb +160 -0
  16. data/lib/clacky/message_history.rb +12 -7
  17. data/lib/clacky/providers.rb +28 -0
  18. data/lib/clacky/rich_ui_controller.rb +3 -1
  19. data/lib/clacky/server/backup_manager.rb +200 -0
  20. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -2
  21. data/lib/clacky/server/channel/adapters/feishu/bot.rb +68 -15
  22. data/lib/clacky/server/channel/channel_manager.rb +180 -81
  23. data/lib/clacky/server/http_server.rb +348 -15
  24. data/lib/clacky/server/scheduler.rb +19 -0
  25. data/lib/clacky/server/session_registry.rb +8 -4
  26. data/lib/clacky/session_manager.rb +40 -2
  27. data/lib/clacky/skill.rb +3 -1
  28. data/lib/clacky/tools/trash_manager.rb +14 -0
  29. data/lib/clacky/ui2/components/command_suggestions.rb +1 -0
  30. data/lib/clacky/ui2/components/modal_component.rb +34 -7
  31. data/lib/clacky/ui2/ui_controller.rb +150 -19
  32. data/lib/clacky/utils/file_processor.rb +75 -4
  33. data/lib/clacky/version.rb +1 -1
  34. data/lib/clacky/web/app.css +2038 -1147
  35. data/lib/clacky/web/app.js +22 -1
  36. data/lib/clacky/web/backup.js +119 -0
  37. data/lib/clacky/web/billing.js +94 -7
  38. data/lib/clacky/web/channels.js +81 -11
  39. data/lib/clacky/web/design-sample.css +247 -0
  40. data/lib/clacky/web/design-sample.html +127 -0
  41. data/lib/clacky/web/favicon.svg +16 -0
  42. data/lib/clacky/web/i18n.js +159 -31
  43. data/lib/clacky/web/index.html +175 -55
  44. data/lib/clacky/web/logo_nav_dark.png +0 -0
  45. data/lib/clacky/web/onboard.js +114 -28
  46. data/lib/clacky/web/sessions.js +436 -192
  47. data/lib/clacky/web/settings.js +21 -1
  48. data/lib/clacky/web/skills.js +6 -6
  49. data/lib/clacky/web/tasks.js +129 -61
  50. data/lib/clacky/web/utils.js +72 -0
  51. data/lib/clacky/web/ws-dispatcher.js +6 -0
  52. data/lib/clacky.rb +1 -0
  53. metadata +8 -3
  54. data/lib/clacky/server/channel/group_message_buffer.rb +0 -53
@@ -20,6 +20,7 @@ const Settings = (() => {
20
20
  _loadBrand();
21
21
  _loadBrowserStatus();
22
22
  _initNetworkSettings();
23
+ if (window.Backup) Backup.load();
23
24
  _applyAboutTabVisibility();
24
25
  }
25
26
 
@@ -1636,10 +1637,11 @@ const Settings = (() => {
1636
1637
  const def = (_mediaDefaults && _mediaDefaults[kind]) || { available: [] };
1637
1638
  const autoAvailable = !!(def && def.model);
1638
1639
  const isCustomEditing = state.source === "custom" && (!state.configured || _mediaCustomDraft[kind]);
1640
+ const isVisionPrimary = kind === "ocr" && state.source === "auto" && state.primary;
1639
1641
 
1640
1642
  const row = document.createElement("div");
1641
1643
  row.className = "media-row";
1642
- if (isCustomEditing || (state.source === "auto" && state.configured) || (state.source === "custom" && state.configured)) {
1644
+ if (!isVisionPrimary && (isCustomEditing || (state.source === "auto" && state.configured) || (state.source === "custom" && state.configured))) {
1643
1645
  row.classList.add("is-expanded");
1644
1646
  }
1645
1647
  row.dataset.kind = kind;
@@ -1653,6 +1655,24 @@ const Settings = (() => {
1653
1655
  title.textContent = I18n.t(`settings.media.kind.${kind}`);
1654
1656
  head.appendChild(title);
1655
1657
 
1658
+ // When the default chat model already supports vision, the OCR sidecar
1659
+ // reuses it automatically — there's nothing to choose, so show a single
1660
+ // read-only note instead of the off/auto/custom switcher.
1661
+ if (isVisionPrimary) {
1662
+ const note = document.createElement("span");
1663
+ note.className = "media-row-status media-vision-primary-note";
1664
+ note.textContent = I18n.t("settings.media.vision.primary");
1665
+ head.appendChild(note);
1666
+
1667
+ const model = document.createElement("span");
1668
+ model.className = "media-vision-primary-model";
1669
+ model.textContent = state.model || "";
1670
+ head.appendChild(model);
1671
+
1672
+ row.appendChild(head);
1673
+ return row;
1674
+ }
1675
+
1656
1676
  const seg = document.createElement("div");
1657
1677
  seg.className = "media-row-segmented";
1658
1678
  ["off", "auto", "custom"].forEach(src => {
@@ -41,8 +41,8 @@ const Skills = (() => {
41
41
  if (showSystemLabel) showSystemLabel.style.display = tab === "my-skills" ? "" : "none";
42
42
  if (refreshBtn) refreshBtn.style.display = tab === "brand-skills" ? "" : "none";
43
43
 
44
- // Lazy-load brand skills when the tab is first opened
45
- if (tab === "brand-skills" && _brandSkills.length === 0) {
44
+ // Refresh brand skills every time the tab is opened
45
+ if (tab === "brand-skills") {
46
46
  _loadBrandSkills();
47
47
  }
48
48
  }
@@ -374,7 +374,7 @@ const Skills = (() => {
374
374
  const toggleTop = toggleLabel.getBoundingClientRect().top;
375
375
  const scrollerTop = scroller.getBoundingClientRect().top;
376
376
  if (toggleTop - scrollerTop < 80) {
377
- toggleLabel.classList.add("skill-toggle-flip");
377
+ toggleLabel.setAttribute("data-tooltip-pos", "bottom");
378
378
  }
379
379
  });
380
380
  }
@@ -395,10 +395,10 @@ const Skills = (() => {
395
395
  if (!container) { console.error("[Skills] skills-list not found!"); return; }
396
396
  container.innerHTML = "";
397
397
 
398
- // Optionally hide system (source=default) skills
398
+ // Optionally hide system (source=default) skills that aren't marked always_show
399
399
  const visible = _showSystemSkills
400
400
  ? _skills
401
- : _skills.filter(s => s.source !== "default");
401
+ : _skills.filter(s => s.always_show || s.source !== "default");
402
402
 
403
403
  if (visible.length === 0) {
404
404
  const emptyText = I18n.t("skills.empty");
@@ -907,7 +907,7 @@ const SkillAC = (() => {
907
907
  .filter(({ score }) => score > 0);
908
908
 
909
909
  if (!_showSystemSkills) {
910
- scored = scored.filter(({ skill }) => skill.source_type !== "default");
910
+ scored = scored.filter(({ skill }) => skill.always_show || skill.source_type !== "default");
911
911
  }
912
912
 
913
913
  // Sort by score descending, stable secondary sort by name
@@ -18,81 +18,158 @@ const Tasks = (() => {
18
18
 
19
19
  // ── Private helpers ────────────────────────────────────────────────────
20
20
 
21
+ function _humanCron(cron) {
22
+ if (!cron) return cron;
23
+ const parts = cron.trim().split(/\s+/);
24
+ if (parts.length !== 5) return cron;
25
+ const [min, hour, dom, month, dow] = parts;
26
+
27
+ const isAny = v => v === "*";
28
+ const isNum = v => /^\d+$/.test(v);
29
+ const pad = n => String(n).padStart(2, "0");
30
+
31
+ const lang = (typeof I18n !== "undefined" && I18n.lang()) || "zh";
32
+ const isZh = lang === "zh";
33
+
34
+ const dowNames = isZh
35
+ ? ["周日","周一","周二","周三","周四","周五","周六"]
36
+ : ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
37
+
38
+ const monthNames = isZh
39
+ ? ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]
40
+ : ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
41
+
42
+ const timeStr = (isNum(hour) && isNum(min))
43
+ ? `${pad(hour)}:${pad(min)}`
44
+ : null;
45
+
46
+ // Every N minutes
47
+ if (min.startsWith("*/") && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
48
+ const n = min.slice(2);
49
+ return isZh ? `每 ${n} 分钟` : `Every ${n} min`;
50
+ }
51
+ // Every N hours
52
+ if (isAny(min) && hour.startsWith("*/") && isAny(dom) && isAny(month) && isAny(dow)) {
53
+ const n = hour.slice(2);
54
+ return isZh ? `每 ${n} 小时` : `Every ${n} hr`;
55
+ }
56
+ // Every minute
57
+ if (isAny(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
58
+ return isZh ? "每分钟" : "Every minute";
59
+ }
60
+ // Every hour at :MM
61
+ if (isNum(min) && isAny(hour) && isAny(dom) && isAny(month) && isAny(dow)) {
62
+ return isZh ? `每小时 :${pad(min)}` : `Hourly at :${pad(min)}`;
63
+ }
64
+ // Specific day-of-week
65
+ if (timeStr && isAny(dom) && isAny(month) && isNum(dow)) {
66
+ const d = dowNames[parseInt(dow, 10)] || dow;
67
+ return isZh ? `每${d} ${timeStr}` : `${d} ${timeStr}`;
68
+ }
69
+ // Weekdays (1-5)
70
+ if (timeStr && isAny(dom) && isAny(month) && dow === "1-5") {
71
+ return isZh ? `工作日 ${timeStr}` : `Weekdays ${timeStr}`;
72
+ }
73
+ // Weekends (0,6 or 6,0)
74
+ if (timeStr && isAny(dom) && isAny(month) && (dow === "0,6" || dow === "6,0")) {
75
+ return isZh ? `周末 ${timeStr}` : `Weekends ${timeStr}`;
76
+ }
77
+ // Every day at HH:MM
78
+ if (timeStr && isAny(dom) && isAny(month) && isAny(dow)) {
79
+ return isZh ? `每天 ${timeStr}` : `Daily ${timeStr}`;
80
+ }
81
+ // Specific day of month
82
+ if (timeStr && isNum(dom) && isAny(month) && isAny(dow)) {
83
+ return isZh ? `每月 ${dom} 日 ${timeStr}` : `Monthly day ${dom} ${timeStr}`;
84
+ }
85
+ // Specific month + day
86
+ if (timeStr && isNum(dom) && isNum(month) && isAny(dow)) {
87
+ const m = monthNames[parseInt(month, 10) - 1] || month;
88
+ return isZh ? `${m}${dom}日 ${timeStr}` : `${m} ${dom} ${timeStr}`;
89
+ }
90
+
91
+ return cron;
92
+ }
93
+
21
94
  /** Render a single task row in the main panel table. */
22
95
  function _renderTaskRow(t) {
23
96
  const row = document.createElement("div");
24
- row.className = "task-table-row";
97
+ row.className = "task-card";
25
98
  row.dataset.name = t.name;
26
99
 
100
+ const isPaused = t.scheduled && t.enabled === false;
101
+ row.classList.toggle("task-card-paused", isPaused);
102
+
27
103
  const schedLabel = t.scheduled
28
- ? escapeHtml(t.cron)
29
- : `<span class="sched-manual">${I18n.t("tasks.manual")}</span>`;
104
+ ? `<span class="task-card-cron" title="${escapeHtml(t.cron)}">${escapeHtml(_humanCron(t.cron))}</span>`
105
+ : `<span class="task-card-cron task-card-cron-manual">${I18n.t("tasks.manual")}</span>`;
30
106
 
31
- // Visual hint: dim the schedule + prepend a "Paused" badge when disabled.
32
- const isPaused = t.scheduled && t.enabled === false;
33
- const schedCell = isPaused
34
- ? `<span class="sched-paused-badge">${I18n.t("tasks.paused")}</span>
35
- <span class="sched-paused-cron">${schedLabel}</span>`
36
- : schedLabel;
107
+ const pausedBadge = isPaused
108
+ ? `<span class="task-card-badge task-card-badge-paused">${I18n.t("tasks.paused")}</span>`
109
+ : "";
37
110
 
38
- const preview = (t.content || "")
111
+ const preview = (t.content || "")
39
112
  .split("\n")
40
113
  .map(l => l.trim())
41
114
  .find(l => l.length > 0) || I18n.t("tasks.empty");
42
- const previewText = preview.length > 80
43
- ? escapeHtml(preview.slice(0, 80)) + "…"
115
+ const previewText = preview.length > 120
116
+ ? escapeHtml(preview.slice(0, 120)) + "…"
44
117
  : escapeHtml(preview);
45
118
 
46
- // Build the pause/resume button only for *scheduled* tasks. Manual tasks
47
- // have no cron to disable.
48
119
  const toggleBtnHtml = t.scheduled ? (isPaused
49
- ? `<button class="task-btn task-btn-toggle task-btn-resume" title="${I18n.t("tasks.btn.resume")}">
50
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
120
+ ? `<button class="task-action-btn task-btn-toggle task-btn-resume">
121
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
51
122
  <polygon points="6 3 20 12 6 21 6 3"/>
52
- </svg><span class="btn-label"> ${I18n.t("tasks.btn.resume")}</span>
123
+ </svg>
124
+ <span>${I18n.t("tasks.btn.resume")}</span>
53
125
  </button>`
54
- : `<button class="task-btn task-btn-toggle task-btn-pause" title="${I18n.t("tasks.btn.pause")}">
55
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
126
+ : `<button class="task-action-btn task-btn-toggle task-btn-pause">
127
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
56
128
  <rect x="6" y="4" width="4" height="16" rx="1"/>
57
129
  <rect x="14" y="4" width="4" height="16" rx="1"/>
58
- </svg><span class="btn-label"> ${I18n.t("tasks.btn.pause")}</span>
130
+ </svg>
131
+ <span>${I18n.t("tasks.btn.pause")}</span>
59
132
  </button>`
60
133
  ) : "";
61
134
 
62
- row.classList.toggle("task-row-paused", isPaused);
63
-
64
135
  row.innerHTML = `
65
- <div class="task-col task-col-name">
66
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="task-icon task-name-icon">
67
- <circle cx="12" cy="12" r="10"/>
68
- <polyline points="12 6 12 12 16 14"/>
69
- </svg>
70
- <div class="task-name-info">
71
- <span class="task-name-text">${escapeHtml(t.name)}</span>
72
- <span class="task-name-sched">${schedCell}</span>
73
- </div>
74
- </div>
75
- <div class="task-col task-col-schedule">${schedCell}</div>
76
- <div class="task-col task-col-content">${previewText}</div>
77
- <div class="task-col task-col-actions">
78
- <button class="task-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
79
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
80
- <polygon points="6 3 20 12 6 21 6 3"/>
81
- </svg><span class="btn-label"> ${I18n.t("tasks.btn.run")}</span>
82
- </button>
83
- ${toggleBtnHtml}
84
- <button class="task-btn task-btn-edit" title="${I18n.t("tasks.btn.edit")}">
85
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
86
- <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
87
- <path d="m15 5 4 4"/>
88
- </svg><span class="btn-label"> ${I18n.t("tasks.btn.edit")}</span>
89
- </button>
90
- <button class="task-btn task-btn-del" title="Delete">
91
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
92
- <path d="M18 6 6 18"/>
93
- <path d="m6 6 12 12"/>
136
+ <div class="task-card-main">
137
+ <div class="task-card-icon">
138
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
139
+ <circle cx="12" cy="12" r="10"/>
140
+ <polyline points="12 6 12 12 16 14"/>
94
141
  </svg>
95
- </button>
142
+ </div>
143
+ <div class="task-card-info">
144
+ <div class="task-card-title-row">
145
+ <span class="task-card-name">${escapeHtml(t.name)}</span>
146
+ ${pausedBadge}
147
+ ${schedLabel}
148
+ </div>
149
+ <div class="task-card-preview">${previewText}</div>
150
+ </div>
151
+ <div class="task-card-actions">
152
+ <button class="task-run-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
153
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
154
+ <polygon points="6 3 20 12 6 21 6 3"/>
155
+ </svg>
156
+ <span>${I18n.t("tasks.btn.run")}</span>
157
+ </button>
158
+ ${toggleBtnHtml}
159
+ <button class="task-action-btn task-btn-edit">
160
+ <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
161
+ <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
162
+ <path d="m15 5 4 4"/>
163
+ </svg>
164
+ <span>${I18n.t("tasks.btn.edit")}</span>
165
+ </button>
166
+ <button class="task-action-btn task-btn-del">
167
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
168
+ <path d="M3 6h18"/><path d="M19 6l-1 14H6L5 6"/><path d="M8 6V4h8v2"/>
169
+ </svg>
170
+ <span>${I18n.t("tasks.btn.delete")}</span>
171
+ </button>
172
+ </div>
96
173
  </div>`;
97
174
 
98
175
  row.querySelector(".task-btn-run").addEventListener("click", e => {
@@ -178,15 +255,6 @@ const Tasks = (() => {
178
255
  return;
179
256
  }
180
257
 
181
- const header = document.createElement("div");
182
- header.className = "task-table-header";
183
- header.innerHTML = `
184
- <div class="task-col task-col-name">${I18n.t("tasks.col.name")}</div>
185
- <div class="task-col task-col-schedule">${I18n.t("tasks.col.schedule")}</div>
186
- <div class="task-col task-col-content">${I18n.t("tasks.col.task")}</div>
187
- <div class="task-col task-col-actions"></div>`;
188
- table.appendChild(header);
189
-
190
258
  _tasks.forEach(t => table.appendChild(_renderTaskRow(t)));
191
259
  },
192
260
 
@@ -55,3 +55,75 @@ const IME = (() => {
55
55
 
56
56
  return { track, bindEnter };
57
57
  })();
58
+
59
+ const Tooltip = (() => {
60
+ const GAP = 8;
61
+ let el = null;
62
+ let _hideTimer = null;
63
+
64
+ function _el() {
65
+ if (!el) el = document.getElementById("tooltip");
66
+ return el;
67
+ }
68
+
69
+ function show(anchor) {
70
+ const tip = _el();
71
+ if (!tip) return;
72
+ clearTimeout(_hideTimer);
73
+
74
+ const text = anchor.getAttribute("data-tooltip");
75
+ if (!text) return;
76
+
77
+ const pos = anchor.getAttribute("data-tooltip-pos") || "top";
78
+ tip.textContent = text;
79
+ tip.setAttribute("data-pos", pos);
80
+ tip.style.display = "block";
81
+
82
+ const r = anchor.getBoundingClientRect();
83
+ const tw = tip.offsetWidth;
84
+ const th = tip.offsetHeight;
85
+
86
+ let top, left;
87
+ if (pos === "bottom") {
88
+ top = r.bottom + GAP;
89
+ left = r.left + r.width / 2 - tw / 2;
90
+ } else if (pos === "left") {
91
+ top = r.top + r.height / 2 - th / 2;
92
+ left = r.left - tw - GAP;
93
+ } else if (pos === "right") {
94
+ top = r.top + r.height / 2 - th / 2;
95
+ left = r.right + GAP;
96
+ } else {
97
+ top = r.top - th - GAP;
98
+ left = r.left + r.width / 2 - tw / 2;
99
+ }
100
+
101
+ left = Math.max(6, Math.min(left, window.innerWidth - tw - 6));
102
+ top = Math.max(6, Math.min(top, window.innerHeight - th - 6));
103
+
104
+ tip.style.left = `${left}px`;
105
+ tip.style.top = `${top}px`;
106
+ requestAnimationFrame(() => tip.classList.add("visible"));
107
+ }
108
+
109
+ function hide() {
110
+ const tip = _el();
111
+ if (!tip) return;
112
+ tip.classList.remove("visible");
113
+ _hideTimer = setTimeout(() => { tip.style.display = "none"; }, 120);
114
+ }
115
+
116
+ function init() {
117
+ document.addEventListener("mouseover", (e) => {
118
+ const anchor = e.target.closest("[data-tooltip]");
119
+ if (anchor) show(anchor);
120
+ });
121
+ document.addEventListener("mouseout", (e) => {
122
+ const anchor = e.target.closest("[data-tooltip]");
123
+ if (!anchor) return;
124
+ if (!anchor.contains(e.relatedTarget)) hide();
125
+ });
126
+ }
127
+
128
+ return { init, show, hide };
129
+ })();
@@ -424,6 +424,12 @@ WS.onEvent(ev => {
424
424
  }
425
425
  });
426
426
 
427
+ // ── Warning transformation ─────────────────────────────────────────────────
428
+
429
+ function _transformRetryWarning(message) {
430
+ return message;
431
+ }
432
+
427
433
  // ── Error rendering ────────────────────────────────────────────────────────
428
434
 
429
435
  function renderErrorEvent(ev) {
data/lib/clacky.rb CHANGED
@@ -136,6 +136,7 @@ require_relative "clacky/agent"
136
136
  require_relative "clacky/server/session_registry"
137
137
  require_relative "clacky/server/web_ui_controller"
138
138
  require_relative "clacky/server/browser_manager"
139
+ require_relative "clacky/server/backup_manager"
139
140
  require_relative "clacky/cli"
140
141
 
141
142
  # Runtime patch layer: load user/AI patches from ~/.clacky/patches/ after all
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.17
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-12 00:00:00.000000000 Z
11
+ date: 2026-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -417,6 +417,7 @@ files:
417
417
  - lib/clacky/mcp/transport.rb
418
418
  - lib/clacky/mcp/virtual_skill.rb
419
419
  - lib/clacky/media/base.rb
420
+ - lib/clacky/media/dashscope.rb
420
421
  - lib/clacky/media/gemini.rb
421
422
  - lib/clacky/media/generator.rb
422
423
  - lib/clacky/media/openai_compat.rb
@@ -431,6 +432,7 @@ files:
431
432
  - lib/clacky/providers.rb
432
433
  - lib/clacky/proxy_config.rb
433
434
  - lib/clacky/rich_ui_controller.rb
435
+ - lib/clacky/server/backup_manager.rb
434
436
  - lib/clacky/server/browser_manager.rb
435
437
  - lib/clacky/server/channel.rb
436
438
  - lib/clacky/server/channel/adapters/base.rb
@@ -455,7 +457,6 @@ files:
455
457
  - lib/clacky/server/channel/channel_config.rb
456
458
  - lib/clacky/server/channel/channel_manager.rb
457
459
  - lib/clacky/server/channel/channel_ui_controller.rb
458
- - lib/clacky/server/channel/group_message_buffer.rb
459
460
  - lib/clacky/server/channel/user_adapter_loader.rb
460
461
  - lib/clacky/server/discover.rb
461
462
  - lib/clacky/server/epipe_safe_io.rb
@@ -539,12 +540,16 @@ files:
539
540
  - lib/clacky/web/app.js
540
541
  - lib/clacky/web/apple-touch-icon-180.png
541
542
  - lib/clacky/web/auth.js
543
+ - lib/clacky/web/backup.js
542
544
  - lib/clacky/web/billing.js
543
545
  - lib/clacky/web/brand.js
544
546
  - lib/clacky/web/channels.js
545
547
  - lib/clacky/web/creator.js
546
548
  - lib/clacky/web/datepicker.js
549
+ - lib/clacky/web/design-sample.css
550
+ - lib/clacky/web/design-sample.html
547
551
  - lib/clacky/web/favicon.ico
552
+ - lib/clacky/web/favicon.svg
548
553
  - lib/clacky/web/i18n.js
549
554
  - lib/clacky/web/icon-dark.svg
550
555
  - lib/clacky/web/icon.svg
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Clacky
4
- module Channel
5
- # Stores recent group chat messages per chat_id so that when the bot is
6
- # @-mentioned it can inject prior conversation context into the agent prompt.
7
- # Thread-safe; bounded to MAX_MESSAGES per chat to limit memory growth.
8
- class GroupMessageBuffer
9
- MAX_MESSAGES = 15
10
- PROMPT_LIMIT = 5
11
-
12
- Entry = Struct.new(:user_id, :user_name, :text, keyword_init: true)
13
-
14
- def initialize
15
- @buffers = {}
16
- @mutex = Mutex.new
17
- end
18
-
19
- # @param chat_id [String]
20
- # @param user_id [String]
21
- # @param user_name [String, nil]
22
- # @param text [String]
23
- def push(chat_id, user_id:, text:, user_name: nil)
24
- return if text.nil? || text.strip.empty?
25
-
26
- @mutex.synchronize do
27
- buf = (@buffers[chat_id] ||= [])
28
- buf << Entry.new(user_id: user_id, user_name: user_name, text: text.strip)
29
- buf.shift if buf.size > MAX_MESSAGES
30
- end
31
- end
32
-
33
- # Return the most recent `limit` entries without clearing the buffer.
34
- # @param chat_id [String]
35
- # @param limit [Integer, nil] max entries to return; nil = all
36
- # @return [Array<Entry>]
37
- def peek(chat_id, limit: nil)
38
- @mutex.synchronize do
39
- buf = @buffers[chat_id] || []
40
- limit ? buf.last(limit) : buf.dup
41
- end
42
- end
43
-
44
- # Return buffered entries for a chat and clear them atomically.
45
- # Returns an empty array when there is no history.
46
- # @param chat_id [String]
47
- # @return [Array<Entry>]
48
- def take(chat_id)
49
- @mutex.synchronize { @buffers.delete(chat_id) || [] }
50
- end
51
- end
52
- end
53
- end