openclacky 1.5.19 → 1.5.21

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +19 -1
  4. data/README_CN.md +1 -1
  5. data/README_JA.md +1 -1
  6. data/lib/clacky/agent/chunk_index.rb +8 -1
  7. data/lib/clacky/agent/feedback.rb +30 -0
  8. data/lib/clacky/agent/history_navigation.rb +17 -3
  9. data/lib/clacky/agent/hook_manager.rb +6 -1
  10. data/lib/clacky/agent/llm_caller.rb +17 -3
  11. data/lib/clacky/agent/message_compressor_helper.rb +36 -2
  12. data/lib/clacky/agent/session_serializer.rb +45 -4
  13. data/lib/clacky/agent.rb +135 -55
  14. data/lib/clacky/agent_config.rb +82 -4
  15. data/lib/clacky/billing/platform_billing.rb +10 -5
  16. data/lib/clacky/brand_config.rb +1 -1
  17. data/lib/clacky/cli.rb +5 -4
  18. data/lib/clacky/client.rb +12 -5
  19. data/lib/clacky/deepseek_web_tools.rb +156 -0
  20. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
  21. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
  22. data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
  23. data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
  24. data/lib/clacky/extension/loader.rb +5 -3
  25. data/lib/clacky/locales/en.rb +5 -5
  26. data/lib/clacky/locales/zh.rb +5 -5
  27. data/lib/clacky/message_format/anthropic.rb +20 -0
  28. data/lib/clacky/message_format/open_ai.rb +31 -14
  29. data/lib/clacky/message_history.rb +45 -4
  30. data/lib/clacky/platform_http_client.rb +6 -2
  31. data/lib/clacky/providers.rb +38 -138
  32. data/lib/clacky/search_config.rb +47 -13
  33. data/lib/clacky/server/browser_manager.rb +35 -5
  34. data/lib/clacky/server/channel/channel_manager.rb +6 -6
  35. data/lib/clacky/server/discover.rb +5 -4
  36. data/lib/clacky/server/http_server.rb +285 -53
  37. data/lib/clacky/server/model_prices.rb +10 -5
  38. data/lib/clacky/server/pid_files.rb +25 -0
  39. data/lib/clacky/server/server_master.rb +9 -3
  40. data/lib/clacky/server/session_registry.rb +37 -7
  41. data/lib/clacky/server/web_ui_controller.rb +72 -0
  42. data/lib/clacky/session_manager.rb +28 -6
  43. data/lib/clacky/skill.rb +1 -1
  44. data/lib/clacky/tools/ask_user.rb +4 -1
  45. data/lib/clacky/tools/base.rb +3 -2
  46. data/lib/clacky/tools/browser.rb +18 -2
  47. data/lib/clacky/tools/file_reader.rb +1 -1
  48. data/lib/clacky/tools/terminal/session_manager.rb +19 -1
  49. data/lib/clacky/tools/terminal.rb +65 -23
  50. data/lib/clacky/tools/trash_manager.rb +5 -3
  51. data/lib/clacky/tools/web_fetch.rb +81 -22
  52. data/lib/clacky/tools/web_search.rb +45 -4
  53. data/lib/clacky/ui2/ui_controller.rb +1 -1
  54. data/lib/clacky/ui_interface.rb +4 -3
  55. data/lib/clacky/utils/file_processor.rb +51 -0
  56. data/lib/clacky/utils/model_pricing.rb +242 -39
  57. data/lib/clacky/version.rb +1 -1
  58. data/lib/clacky/web/app.css +1158 -249
  59. data/lib/clacky/web/app.js +82 -13
  60. data/lib/clacky/web/components/chat-navigator.js +8 -1
  61. data/lib/clacky/web/components/code-editor.js +6 -2
  62. data/lib/clacky/web/components/mentions.js +2 -13
  63. data/lib/clacky/web/components/model-picker.js +92 -97
  64. data/lib/clacky/web/components/onboard.js +13 -10
  65. data/lib/clacky/web/components/sidebar.js +6 -2
  66. data/lib/clacky/web/features/archive/store.js +196 -0
  67. data/lib/clacky/web/features/archive/view.js +409 -0
  68. data/lib/clacky/web/features/backup/view.js +17 -7
  69. data/lib/clacky/web/features/billing/view.js +84 -10
  70. data/lib/clacky/web/features/channels/view.js +75 -29
  71. data/lib/clacky/web/features/new-session/store.js +3 -3
  72. data/lib/clacky/web/features/new-session/view.js +1 -0
  73. data/lib/clacky/web/features/trash/store.js +14 -11
  74. data/lib/clacky/web/features/trash/view.js +66 -13
  75. data/lib/clacky/web/features/workspace/view.js +421 -16
  76. data/lib/clacky/web/i18n.js +242 -70
  77. data/lib/clacky/web/index.html +270 -117
  78. data/lib/clacky/web/projects.js +56 -1
  79. data/lib/clacky/web/sessions.js +609 -267
  80. data/lib/clacky/web/settings.js +186 -32
  81. data/lib/clacky/web/ws-dispatcher.js +33 -21
  82. data/lib/clacky.rb +4 -2
  83. metadata +6 -1
@@ -27,19 +27,33 @@ const Settings = (() => {
27
27
  let _modelSearchQuery = "";
28
28
  // Provider id selected in the model provider filter; empty = show all.
29
29
  let _modelProviderFilter = "";
30
+ // Last successfully activated tab. Dynamic extension tabs may be rebuilt
31
+ // while Settings is hidden, so the DOM alone cannot retain this state.
32
+ let _activeTab = "models";
30
33
 
31
34
  // ── Public API ──────────────────────────────────────────────────────────────
32
35
 
33
- function open() {
34
- _load();
35
- _loadMedia();
36
+ function open(options = {}) {
37
+ const targetTab = options.tab || _activeTab;
38
+ if (!_activateTab(targetTab) && targetTab !== "models") _activateTab("models");
39
+
40
+ const loadModels = _load();
41
+ const loadMedia = _loadMedia();
36
42
  _loadBrand();
37
43
  _loadBrowserStatus();
38
44
  _initNetworkSettings();
39
45
  _initSearchSettings();
40
46
  _initPlatformSourceSettings();
41
- if (Clacky.Backup) Clacky.Backup.load();
42
47
  _applyAboutTabVisibility();
48
+
49
+ if (options.section) {
50
+ Promise.all([loadModels, loadMedia]).then(() => {
51
+ requestAnimationFrame(() => {
52
+ const section = document.getElementById(options.section);
53
+ if (section) section.scrollIntoView({ behavior: "smooth", block: "start" });
54
+ });
55
+ });
56
+ }
43
57
  }
44
58
 
45
59
  // ── Data Loading ────────────────────────────────────────────────────────────
@@ -268,8 +282,11 @@ const Settings = (() => {
268
282
  </button>` : ""}
269
283
  </div>
270
284
  <div class="model-card-grid-provider">${_esc(providerName)}</div>
271
- ${remark ? `<div class="model-card-grid-remark" title="${_esc(remark)}">${_esc(remark)}</div>` : ""}
272
285
  ${model.api_key_masked ? `<div class="model-card-grid-model">${_esc(model.api_key_masked)}</div>` : ""}
286
+ ${remark ? `<div class="model-card-grid-remark" title="${_esc(remark)}">
287
+ <span class="model-card-grid-remark-label">${I18n.t("settings.models.field.remark")}</span>
288
+ <span class="model-card-grid-remark-text">${_esc(remark)}</span>
289
+ </div>` : ""}
273
290
  <div class="model-card-grid-status">
274
291
  <span class="model-test-result" data-index="${index}"></span>
275
292
  </div>
@@ -1582,12 +1599,14 @@ const Settings = (() => {
1582
1599
  const btn = document.getElementById("btn-browser-setup");
1583
1600
  const toggleWrap = document.getElementById("browser-toggle-wrap");
1584
1601
  const toggleInput = document.getElementById("browser-toggle-input");
1602
+ const shotWrap = document.getElementById("browser-screenshot-settings");
1585
1603
 
1586
1604
  // File doesn't exist → not set up yet
1587
1605
  if (data.enabled === undefined || data.enabled === null) {
1588
1606
  desc.textContent = I18n.t("settings.browser.desc");
1589
1607
  btn.textContent = I18n.t("settings.browser.btn");
1590
1608
  toggleWrap.style.display = "none";
1609
+ shotWrap.style.display = "none";
1591
1610
  return;
1592
1611
  }
1593
1612
 
@@ -1603,9 +1622,51 @@ const Settings = (() => {
1603
1622
  toggleInput.dataset.bound = "1";
1604
1623
  toggleInput.addEventListener("change", _toggleBrowser);
1605
1624
  }
1625
+
1626
+ _renderScreenshotSettings(data);
1606
1627
  } catch (_) { /* non-critical */ }
1607
1628
  }
1608
1629
 
1630
+ function _renderScreenshotSettings(data) {
1631
+ const wrap = document.getElementById("browser-screenshot-settings");
1632
+ const quality = document.getElementById("browser-screenshot-quality");
1633
+
1634
+ wrap.style.display = "flex";
1635
+
1636
+ const limit = data.screenshot_max_base64_bytes;
1637
+ quality.querySelectorAll(".settings-segmented-btn").forEach((btn) => {
1638
+ btn.classList.toggle("active", Number(btn.dataset.bytes) === limit);
1639
+ });
1640
+
1641
+ if (!quality.dataset.bound) {
1642
+ quality.dataset.bound = "1";
1643
+ quality.addEventListener("click", _onScreenshotQualityClick);
1644
+ }
1645
+ }
1646
+
1647
+ async function _onScreenshotQualityClick(event) {
1648
+ const btn = event.target.closest(".settings-segmented-btn");
1649
+ if (!btn || btn.classList.contains("active")) return;
1650
+ await _saveScreenshotSettings({ screenshot_max_base64_bytes: Number(btn.dataset.bytes) });
1651
+ }
1652
+
1653
+ async function _saveScreenshotSettings(payload) {
1654
+ try {
1655
+ const res = await fetch("/api/browser/settings", {
1656
+ method: "POST",
1657
+ headers: { "Content-Type": "application/json" },
1658
+ body: JSON.stringify(payload)
1659
+ });
1660
+ const data = await res.json();
1661
+ if (!data.ok) throw new Error(data.error || "save failed");
1662
+ _renderScreenshotSettings(data);
1663
+ Modal.toast(I18n.t("settings.browser.screenshot.saved"), "success");
1664
+ } catch (_) {
1665
+ Modal.toast(I18n.t("settings.browser.screenshot.saveFailed"), "error");
1666
+ await _loadBrowserStatus();
1667
+ }
1668
+ }
1669
+
1609
1670
  async function _toggleBrowser() {
1610
1671
  const toggleInput = document.getElementById("browser-toggle-input");
1611
1672
  toggleInput.disabled = true;
@@ -1674,14 +1735,14 @@ const Settings = (() => {
1674
1735
  const data = await res.json();
1675
1736
  if (data.ok) {
1676
1737
  status.textContent = I18n.t(successKey);
1677
- status.className = "model-test-result success";
1738
+ status.className = "model-test-result result-ok";
1678
1739
  } else {
1679
1740
  status.textContent = data.error || I18n.t("settings.network.invalidUrl");
1680
- status.className = "model-test-result error";
1741
+ status.className = "model-test-result result-fail";
1681
1742
  }
1682
1743
  } catch (e) {
1683
1744
  status.textContent = e.message || I18n.t("settings.network.invalidUrl");
1684
- status.className = "model-test-result error";
1745
+ status.className = "model-test-result result-fail";
1685
1746
  }
1686
1747
  }
1687
1748
 
@@ -1711,8 +1772,17 @@ const Settings = (() => {
1711
1772
  const saveBtn = document.getElementById("btn-save-search");
1712
1773
  const testBtn = document.getElementById("btn-test-search");
1713
1774
  const status = document.getElementById("settings-search-status");
1775
+ const dsHint = document.getElementById("search-deepseek-hint");
1776
+ const builtinNote = document.getElementById("search-builtin-note");
1777
+ const customBtn = document.getElementById("btn-custom-searcher");
1714
1778
  if (!dropdown || !saveBtn) return;
1715
1779
 
1780
+ const DS_WEB = "deepseek_web";
1781
+ // Per-call billing for hosted web tools starts 2026-11-01 00:00 Beijing
1782
+ // time (UTC+8); before that the platform records calls at cost 0.
1783
+ const DS_BILLING_START_MS = Date.parse("2026-10-31T16:00:00Z");
1784
+ const defaultKeyPlaceholder = keyInput ? keyInput.placeholder : "";
1785
+
1716
1786
  let select = dropdown.__customSelect;
1717
1787
  if (!select && trigger) {
1718
1788
  select = CustomSelect.init({
@@ -1723,7 +1793,60 @@ const Settings = (() => {
1723
1793
  }
1724
1794
 
1725
1795
  function _syncKeyVisibility() {
1726
- keyWrap.style.display = (select && select.value) ? "" : "none";
1796
+ const value = select ? select.value : "";
1797
+ keyWrap.style.display = value ? "" : "none";
1798
+
1799
+ // Keep the trigger's i18n key in step with the selection, so a language
1800
+ // switch does not relabel the closed dropdown with another provider's name.
1801
+ const valueSpan = document.getElementById("settings-search-provider-value");
1802
+ if (valueSpan) {
1803
+ if (value === DS_WEB) {
1804
+ valueSpan.setAttribute("data-i18n", "settings.search.deepseek");
1805
+ } else if (value) {
1806
+ valueSpan.removeAttribute("data-i18n");
1807
+ } else {
1808
+ valueSpan.setAttribute("data-i18n", "settings.search.builtin");
1809
+ }
1810
+ }
1811
+
1812
+ const isDeepSeek = value === DS_WEB;
1813
+ if (dsHint) {
1814
+ dsHint.style.display = isDeepSeek ? "" : "none";
1815
+ if (isDeepSeek) {
1816
+ const hintKey = Date.now() < DS_BILLING_START_MS
1817
+ ? "settings.search.deepseekHintFree"
1818
+ : "settings.search.deepseekHint";
1819
+ dsHint.setAttribute("data-i18n", hintKey);
1820
+ dsHint.textContent = I18n.t(hintKey);
1821
+ }
1822
+ }
1823
+ if (builtinNote) builtinNote.style.display = value ? "none" : "";
1824
+ if (keyInput) {
1825
+ keyInput.placeholder = isDeepSeek
1826
+ ? I18n.t("settings.search.deepseekKeyPlaceholder")
1827
+ : defaultKeyPlaceholder;
1828
+ }
1829
+ // Anything already shown refers to the provider the user just left.
1830
+ if (status) {
1831
+ status.textContent = "";
1832
+ status.className = "model-test-result";
1833
+ }
1834
+ }
1835
+
1836
+ // Assigning onclick (not addEventListener) keeps re-opening Settings from
1837
+ // stacking duplicate handlers, since open() re-runs this initializer.
1838
+ if (customBtn) {
1839
+ customBtn.onclick = async () => {
1840
+ customBtn.disabled = true;
1841
+ try {
1842
+ Router.navigate("chat");
1843
+ await Sessions.startWith(I18n.t("settings.search.customPrompt"), {
1844
+ name: I18n.t("settings.search.customSessionName"),
1845
+ });
1846
+ } finally {
1847
+ customBtn.disabled = false;
1848
+ }
1849
+ };
1727
1850
  }
1728
1851
 
1729
1852
  try {
@@ -1732,8 +1855,12 @@ const Settings = (() => {
1732
1855
  if (data.ok) {
1733
1856
  const rows = (data.available || []).map(name =>
1734
1857
  `<div class="custom-select-option" data-value="${_esc(name)}">${_esc(name)}</div>`).join("");
1858
+ const deepseekRow =
1859
+ `<div class="custom-select-option" data-value="${DS_WEB}">${_esc(I18n.t("settings.search.deepseek"))}</div>`;
1735
1860
  dropdown.innerHTML =
1736
- `<div class="custom-select-option" data-value="">${I18n.t("settings.search.builtin")}</div>` + rows;
1861
+ `<div class="custom-select-option" data-value="">${I18n.t("settings.search.builtin")}</div>` +
1862
+ deepseekRow +
1863
+ rows;
1737
1864
  if (select) select.setValue(data.provider || "");
1738
1865
  keyInput.value = data.key_masked || "";
1739
1866
  }
@@ -1742,7 +1869,7 @@ const Settings = (() => {
1742
1869
 
1743
1870
  function _showStatus(message, ok) {
1744
1871
  status.textContent = message;
1745
- status.className = `model-test-result ${ok ? "success" : "error"}`;
1872
+ status.className = `model-test-result ${ok ? "result-ok" : "result-fail"}`;
1746
1873
  }
1747
1874
 
1748
1875
  if (!saveBtn.dataset.bound) {
@@ -1750,15 +1877,26 @@ const Settings = (() => {
1750
1877
  saveBtn.addEventListener("click", async () => {
1751
1878
  status.textContent = "";
1752
1879
  status.className = "model-test-result";
1880
+
1881
+ const provider = select.value;
1882
+ const key = keyInput.value.trim();
1883
+ // DeepSeek falls back to the configured model key, so an empty field is
1884
+ // valid there. Every other searcher is a script that reads the key from
1885
+ // CLACKY_SEARCH_KEY and cannot run without one.
1886
+ if (provider && provider !== DS_WEB && !key) {
1887
+ _showStatus(I18n.t("settings.search.keyRequired"), false);
1888
+ return;
1889
+ }
1890
+
1753
1891
  try {
1754
1892
  const res = await fetch("/api/config/search", {
1755
1893
  method: "PATCH",
1756
1894
  headers: { "Content-Type": "application/json" },
1757
- body: JSON.stringify({ provider: select.value, key: keyInput.value })
1895
+ body: JSON.stringify({ provider: select.value, key: key })
1758
1896
  });
1759
1897
  const data = await res.json();
1760
1898
  if (data.ok) {
1761
- _showStatus(I18n.t("settings.search.saved"), true);
1899
+ Modal.toast(I18n.t("settings.search.saved"), "success");
1762
1900
  } else {
1763
1901
  _showStatus(data.error || I18n.t("settings.search.failed"), false);
1764
1902
  }
@@ -1970,6 +2108,20 @@ const Settings = (() => {
1970
2108
  badge.className = "brand-status-value badge-active";
1971
2109
  }
1972
2110
 
2111
+ // Only "unreachable" is ambiguous on its own: it means heartbeats have
2112
+ // been failing past the grace period, not that the license is invalid.
2113
+ const hint = document.getElementById("brand-status-hint");
2114
+ if (hint) {
2115
+ if (data.warning && data.warning_type === "unreachable") {
2116
+ // CJK copy reads best with full-width brackets; latin locales don't.
2117
+ const note = I18n.t("settings.brand.hint.unreachable");
2118
+ hint.textContent = I18n.lang() === "zh" ? `(${note})` : `(${note})`;
2119
+ hint.style.display = "";
2120
+ } else {
2121
+ hint.style.display = "none";
2122
+ }
2123
+ }
2124
+
1973
2125
  // Fetch full brand info for expiry date and support QR code
1974
2126
  fetch("/api/brand").then(r => r.json()).then(info => {
1975
2127
  const expiresEl = document.getElementById("brand-status-expires");
@@ -2134,6 +2286,25 @@ const Settings = (() => {
2134
2286
 
2135
2287
  // ── Init ──────────────────────────────────────────────────────────────────────
2136
2288
 
2289
+ function _activateTab(targetTab) {
2290
+ const tab = Array.from(document.querySelectorAll("#settings-tabs .settings-tab"))
2291
+ .find(candidate => candidate.dataset.tab === targetTab);
2292
+ const panel = Array.from(document.querySelectorAll("#settings-body .settings-tab-content"))
2293
+ .find(candidate => candidate.dataset.tabContent === targetTab);
2294
+ if (!tab || !panel) return false;
2295
+
2296
+ document.querySelectorAll("#settings-tabs .settings-tab").forEach(candidate =>
2297
+ candidate.classList.toggle("active", candidate === tab));
2298
+
2299
+ document.querySelectorAll("#settings-body .settings-tab-content").forEach(content => {
2300
+ const isActive = content.dataset.tabContent === targetTab;
2301
+ content.classList.toggle("active", isActive);
2302
+ content.style.display = isActive ? "" : "none";
2303
+ });
2304
+ _activeTab = targetTab;
2305
+ return true;
2306
+ }
2307
+
2137
2308
  function _initTabs() {
2138
2309
  const bar = document.getElementById("settings-tabs");
2139
2310
  if (!bar) return;
@@ -2145,15 +2316,7 @@ const Settings = (() => {
2145
2316
  if (!tab || !bar.contains(tab)) return;
2146
2317
  const targetTab = tab.dataset.tab;
2147
2318
  if (!targetTab) return;
2148
-
2149
- document.querySelectorAll("#settings-tabs .settings-tab").forEach(t =>
2150
- t.classList.toggle("active", t.dataset.tab === targetTab));
2151
-
2152
- document.querySelectorAll("#settings-body .settings-tab-content").forEach(c => {
2153
- const isActive = c.dataset.tabContent === targetTab;
2154
- c.classList.toggle("active", isActive);
2155
- c.style.display = isActive ? "" : "none";
2156
- });
2319
+ _activateTab(targetTab);
2157
2320
  });
2158
2321
  }
2159
2322
 
@@ -2166,16 +2329,7 @@ const Settings = (() => {
2166
2329
  if (branded) {
2167
2330
  tabBtn.style.display = "none";
2168
2331
  if (tabBtn.classList.contains("active")) {
2169
- tabBtn.classList.remove("active");
2170
- tabPanel.classList.remove("active");
2171
- tabPanel.style.display = "none";
2172
- const fallback = document.querySelector('#settings-tabs .settings-tab[data-tab="models"]');
2173
- const fallbackPanel = document.querySelector('#settings-body .settings-tab-content[data-tab-content="models"]');
2174
- if (fallback) fallback.classList.add("active");
2175
- if (fallbackPanel) {
2176
- fallbackPanel.classList.add("active");
2177
- fallbackPanel.style.display = "";
2178
- }
2332
+ _activateTab("models");
2179
2333
  }
2180
2334
  } else {
2181
2335
  tabBtn.style.display = "";
@@ -289,7 +289,7 @@ function _dispatchEvent(ev) {
289
289
 
290
290
  // ── Session list ───────────────────────────────────────────────────
291
291
  case "session_list": {
292
- Sessions.setAll(ev.sessions || [], !!ev.has_more, ev.groups || {});
292
+ Sessions.setAll(ev.sessions || [], !!ev.has_more, ev.groups || {}, ev.next_cursor || null);
293
293
  Projects.setAll(ev.projects || []);
294
294
  Sessions.renderList();
295
295
  Projects.renderSection();
@@ -369,18 +369,11 @@ function _dispatchEvent(ev) {
369
369
  }
370
370
  if (!sid) break;
371
371
  // A creation update is emitted only after the session's initial metadata
372
- // has been persisted. Promote it into the canonical list; ordinary full
373
- // snapshots remain patches so reconnects and old-session updates cannot
374
- // disturb the pagination boundary.
375
- //
376
- // Exception: the session the user is currently viewing. It may have been
377
- // opened from search / a deep link and live only in the `_extraSessions`
378
- // cache (outside the loaded page), which the sidebar does not render —
379
- // patching it would leave the list without a row until a reload. A full
380
- // snapshot carries every column the list needs, and its fresh
381
- // `updated_at` sorts it to the top, exactly where a reload would put it.
372
+ // has been persisted. Other full snapshots join the canonical list only
373
+ // when they sort inside the loaded pages — exactly where a reload would
374
+ // put them — so stale snapshots cannot disturb the pagination boundary.
382
375
  const listed = Sessions.all.some(s => s.id === sid);
383
- if (ev.session && (ev.created || (sid === Sessions.activeId && !listed))) {
376
+ if (ev.session && (ev.created || (!listed && Sessions.belongsToLoadedPage(ev.session)))) {
384
377
  Sessions.add(ev.session);
385
378
  } else {
386
379
  Sessions.patch(sid, patch);
@@ -418,7 +411,6 @@ function _dispatchEvent(ev) {
418
411
  Sessions.patch(ev.session_id, { name: ev.name });
419
412
  Sessions.renderList();
420
413
  Projects.renderSection();
421
- // Title is now shown only in the sidebar; chat-header element was removed.
422
414
  break;
423
415
  }
424
416
 
@@ -672,6 +664,10 @@ function _dispatchEvent(ev) {
672
664
  if (typeof Share !== "undefined") Share.maybePromptOnComplete();
673
665
  break;
674
666
 
667
+ case "feedback_countdown":
668
+ Sessions.updateFeedbackCountdown(ev);
669
+ break;
670
+
675
671
  case "request_feedback":
676
672
  if (ev.session_id !== Sessions.activeId) break;
677
673
  Sessions.showFeedbackRequest(ev.question, ev.context, ev.options, ev.questions);
@@ -777,20 +773,36 @@ function _transformRetryWarning(message) {
777
773
 
778
774
  // ── Error rendering ────────────────────────────────────────────────────────
779
775
 
780
- function renderErrorEvent(ev) {
781
- const topUp = ev.top_up_url
782
- ? `<a class="error-action-btn" href="${escapeHtml(ev.top_up_url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(I18n.t("error.insufficient_credit.action"))}</a>`
783
- : "";
776
+ // Credit / allow-list failures lead to the top-up page; any other error that
777
+ // carries a console URL (e.g. a network failure the user may resolve by
778
+ // checking their key or balance) reuses it under a neutral label.
779
+ function _errorActionBtn(ev) {
780
+ if (!ev.top_up_url) return "";
781
+ const labelKey = ev.code === "insufficient_credit" || ev.code === "model_not_allowed"
782
+ ? "error.insufficient_credit.action"
783
+ : "error.check_account.action";
784
+ return `<a class="error-action-btn" href="${escapeHtml(ev.top_up_url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(I18n.t(labelKey))}</a>`;
785
+ }
786
+
787
+ function renderErrorEvent(ev, container = null) {
788
+ const append = html => {
789
+ if (!container) return Sessions.appendMsg("error", html);
790
+ const el = document.createElement("div");
791
+ el.className = "msg msg-error msg-history-error";
792
+ el.innerHTML = html;
793
+ container.appendChild(el);
794
+ };
795
+ const action = container ? "" : _errorActionBtn(ev);
784
796
  if (ev.code === "insufficient_credit") {
785
- const body = escapeHtml(I18n.t("error.insufficient_credit"));
786
- Sessions.appendMsg("error", `<span>${body}</span>${topUp}${_buildRawDetail(ev.raw_message)}`);
797
+ const body = escapeHtml(I18n.t(container ? "error.insufficient_credit.history" : "error.insufficient_credit"));
798
+ append(`<span class="error-credit-message">${body}</span>${action}${_buildRawDetail(ev.raw_message)}`);
787
799
  return;
788
800
  }
789
801
  if (ev.code === "model_not_allowed") {
790
- Sessions.appendMsg("error", `<span>${escapeHtml(ev.message)}</span>${topUp}${_buildRawDetail(ev.raw_message)}`);
802
+ append(`<span>${escapeHtml(ev.message)}</span>${action}${_buildRawDetail(ev.raw_message)}`);
791
803
  return;
792
804
  }
793
- Sessions.appendMsg("error", escapeHtml(ev.message) + _buildRawDetail(ev.raw_message));
805
+ append(escapeHtml(ev.message) + action + _buildRawDetail(ev.raw_message));
794
806
  }
795
807
 
796
808
  function _buildRawDetail(raw) {
data/lib/clacky.rb CHANGED
@@ -111,6 +111,7 @@ require_relative "clacky/utils/browser_detector"
111
111
  require_relative "clacky/utils/scripts_manager"
112
112
  require_relative "clacky/utils/searcher_manager"
113
113
  require_relative "clacky/search_config"
114
+ require_relative "clacky/deepseek_web_tools"
114
115
  require_relative "clacky/utils/model_pricing"
115
116
  require_relative "clacky/utils/gitignore_parser"
116
117
  require_relative "clacky/utils/limit_stack"
@@ -179,11 +180,12 @@ Clacky::ExtensionHookLoader.load_all
179
180
  module Clacky
180
181
  class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
181
182
  class AgentError < StandardError
182
- attr_reader :raw_message
183
+ attr_reader :raw_message, :action_url
183
184
 
184
- def initialize(message, raw_message: nil)
185
+ def initialize(message, raw_message: nil, action_url: nil)
185
186
  super(message)
186
187
  @raw_message = raw_message
188
+ @action_url = action_url
187
189
  end
188
190
  end
189
191
  class BadRequestError < AgentError
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.19
4
+ version: 1.5.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -346,6 +346,7 @@ files:
346
346
  - lib/clacky/agent/chunk_index.rb
347
347
  - lib/clacky/agent/cost_tracker.rb
348
348
  - lib/clacky/agent/fake_tool_call_detector.rb
349
+ - lib/clacky/agent/feedback.rb
349
350
  - lib/clacky/agent/goal_manager.rb
350
351
  - lib/clacky/agent/goal_state.rb
351
352
  - lib/clacky/agent/history_navigation.rb
@@ -377,6 +378,7 @@ files:
377
378
  - lib/clacky/cli.rb
378
379
  - lib/clacky/cli_guidance.rb
379
380
  - lib/clacky/client.rb
381
+ - lib/clacky/deepseek_web_tools.rb
380
382
  - lib/clacky/default_extensions/advisor/advisors/general.md
381
383
  - lib/clacky/default_extensions/advisor/api/handler.rb
382
384
  - lib/clacky/default_extensions/advisor/ext.yml
@@ -590,6 +592,7 @@ files:
590
592
  - lib/clacky/server/git_panel.rb
591
593
  - lib/clacky/server/http_server.rb
592
594
  - lib/clacky/server/model_prices.rb
595
+ - lib/clacky/server/pid_files.rb
593
596
  - lib/clacky/server/preview.rb
594
597
  - lib/clacky/server/project_manager.rb
595
598
  - lib/clacky/server/scheduler.rb
@@ -697,6 +700,8 @@ files:
697
700
  - lib/clacky/web/design-sample.html
698
701
  - lib/clacky/web/favicon.ico
699
702
  - lib/clacky/web/favicon.svg
703
+ - lib/clacky/web/features/archive/store.js
704
+ - lib/clacky/web/features/archive/view.js
700
705
  - lib/clacky/web/features/backup/store.js
701
706
  - lib/clacky/web/features/backup/view.js
702
707
  - lib/clacky/web/features/billing/store.js