openclacky 1.5.20 → 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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +18 -0
  4. data/lib/clacky/agent/chunk_index.rb +8 -1
  5. data/lib/clacky/agent/feedback.rb +30 -0
  6. data/lib/clacky/agent/history_navigation.rb +17 -3
  7. data/lib/clacky/agent/hook_manager.rb +6 -1
  8. data/lib/clacky/agent/llm_caller.rb +1 -0
  9. data/lib/clacky/agent/message_compressor_helper.rb +28 -2
  10. data/lib/clacky/agent/session_serializer.rb +20 -2
  11. data/lib/clacky/agent.rb +56 -43
  12. data/lib/clacky/agent_config.rb +82 -4
  13. data/lib/clacky/billing/platform_billing.rb +5 -1
  14. data/lib/clacky/brand_config.rb +1 -1
  15. data/lib/clacky/cli.rb +3 -2
  16. data/lib/clacky/client.rb +5 -5
  17. data/lib/clacky/deepseek_web_tools.rb +156 -0
  18. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
  19. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +21 -1
  20. data/lib/clacky/extension/loader.rb +5 -3
  21. data/lib/clacky/locales/en.rb +5 -5
  22. data/lib/clacky/locales/zh.rb +5 -5
  23. data/lib/clacky/message_format/anthropic.rb +20 -0
  24. data/lib/clacky/message_format/open_ai.rb +31 -14
  25. data/lib/clacky/message_history.rb +21 -3
  26. data/lib/clacky/platform_http_client.rb +6 -2
  27. data/lib/clacky/providers.rb +23 -14
  28. data/lib/clacky/search_config.rb +47 -13
  29. data/lib/clacky/server/browser_manager.rb +35 -5
  30. data/lib/clacky/server/channel/channel_manager.rb +6 -6
  31. data/lib/clacky/server/discover.rb +5 -4
  32. data/lib/clacky/server/http_server.rb +194 -47
  33. data/lib/clacky/server/model_prices.rb +5 -3
  34. data/lib/clacky/server/pid_files.rb +25 -0
  35. data/lib/clacky/server/server_master.rb +9 -3
  36. data/lib/clacky/server/session_registry.rb +19 -7
  37. data/lib/clacky/server/web_ui_controller.rb +61 -38
  38. data/lib/clacky/session_manager.rb +7 -1
  39. data/lib/clacky/skill.rb +1 -1
  40. data/lib/clacky/tools/ask_user.rb +4 -1
  41. data/lib/clacky/tools/base.rb +3 -2
  42. data/lib/clacky/tools/browser.rb +18 -2
  43. data/lib/clacky/tools/file_reader.rb +1 -1
  44. data/lib/clacky/tools/terminal/session_manager.rb +19 -1
  45. data/lib/clacky/tools/terminal.rb +65 -23
  46. data/lib/clacky/tools/trash_manager.rb +5 -3
  47. data/lib/clacky/tools/web_fetch.rb +81 -22
  48. data/lib/clacky/tools/web_search.rb +45 -4
  49. data/lib/clacky/ui2/ui_controller.rb +1 -1
  50. data/lib/clacky/ui_interface.rb +4 -3
  51. data/lib/clacky/utils/file_processor.rb +51 -0
  52. data/lib/clacky/utils/model_pricing.rb +132 -13
  53. data/lib/clacky/version.rb +1 -1
  54. data/lib/clacky/web/app.css +366 -105
  55. data/lib/clacky/web/components/chat-navigator.js +8 -1
  56. data/lib/clacky/web/components/mentions.js +2 -13
  57. data/lib/clacky/web/components/model-picker.js +3 -1
  58. data/lib/clacky/web/components/onboard.js +13 -10
  59. data/lib/clacky/web/features/archive/store.js +30 -16
  60. data/lib/clacky/web/features/archive/view.js +20 -10
  61. data/lib/clacky/web/features/backup/view.js +5 -5
  62. data/lib/clacky/web/features/new-session/store.js +3 -3
  63. data/lib/clacky/web/features/trash/store.js +10 -7
  64. data/lib/clacky/web/features/trash/view.js +9 -4
  65. data/lib/clacky/web/i18n.js +106 -30
  66. data/lib/clacky/web/index.html +90 -60
  67. data/lib/clacky/web/projects.js +56 -1
  68. data/lib/clacky/web/sessions.js +244 -131
  69. data/lib/clacky/web/settings.js +143 -8
  70. data/lib/clacky/web/ws-dispatcher.js +9 -13
  71. data/lib/clacky.rb +1 -0
  72. metadata +4 -1
@@ -1599,12 +1599,14 @@ const Settings = (() => {
1599
1599
  const btn = document.getElementById("btn-browser-setup");
1600
1600
  const toggleWrap = document.getElementById("browser-toggle-wrap");
1601
1601
  const toggleInput = document.getElementById("browser-toggle-input");
1602
+ const shotWrap = document.getElementById("browser-screenshot-settings");
1602
1603
 
1603
1604
  // File doesn't exist → not set up yet
1604
1605
  if (data.enabled === undefined || data.enabled === null) {
1605
1606
  desc.textContent = I18n.t("settings.browser.desc");
1606
1607
  btn.textContent = I18n.t("settings.browser.btn");
1607
1608
  toggleWrap.style.display = "none";
1609
+ shotWrap.style.display = "none";
1608
1610
  return;
1609
1611
  }
1610
1612
 
@@ -1620,9 +1622,51 @@ const Settings = (() => {
1620
1622
  toggleInput.dataset.bound = "1";
1621
1623
  toggleInput.addEventListener("change", _toggleBrowser);
1622
1624
  }
1625
+
1626
+ _renderScreenshotSettings(data);
1623
1627
  } catch (_) { /* non-critical */ }
1624
1628
  }
1625
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
+
1626
1670
  async function _toggleBrowser() {
1627
1671
  const toggleInput = document.getElementById("browser-toggle-input");
1628
1672
  toggleInput.disabled = true;
@@ -1691,14 +1735,14 @@ const Settings = (() => {
1691
1735
  const data = await res.json();
1692
1736
  if (data.ok) {
1693
1737
  status.textContent = I18n.t(successKey);
1694
- status.className = "model-test-result success";
1738
+ status.className = "model-test-result result-ok";
1695
1739
  } else {
1696
1740
  status.textContent = data.error || I18n.t("settings.network.invalidUrl");
1697
- status.className = "model-test-result error";
1741
+ status.className = "model-test-result result-fail";
1698
1742
  }
1699
1743
  } catch (e) {
1700
1744
  status.textContent = e.message || I18n.t("settings.network.invalidUrl");
1701
- status.className = "model-test-result error";
1745
+ status.className = "model-test-result result-fail";
1702
1746
  }
1703
1747
  }
1704
1748
 
@@ -1728,8 +1772,17 @@ const Settings = (() => {
1728
1772
  const saveBtn = document.getElementById("btn-save-search");
1729
1773
  const testBtn = document.getElementById("btn-test-search");
1730
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");
1731
1778
  if (!dropdown || !saveBtn) return;
1732
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
+
1733
1786
  let select = dropdown.__customSelect;
1734
1787
  if (!select && trigger) {
1735
1788
  select = CustomSelect.init({
@@ -1740,7 +1793,60 @@ const Settings = (() => {
1740
1793
  }
1741
1794
 
1742
1795
  function _syncKeyVisibility() {
1743
- 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
+ };
1744
1850
  }
1745
1851
 
1746
1852
  try {
@@ -1749,8 +1855,12 @@ const Settings = (() => {
1749
1855
  if (data.ok) {
1750
1856
  const rows = (data.available || []).map(name =>
1751
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>`;
1752
1860
  dropdown.innerHTML =
1753
- `<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;
1754
1864
  if (select) select.setValue(data.provider || "");
1755
1865
  keyInput.value = data.key_masked || "";
1756
1866
  }
@@ -1759,7 +1869,7 @@ const Settings = (() => {
1759
1869
 
1760
1870
  function _showStatus(message, ok) {
1761
1871
  status.textContent = message;
1762
- status.className = `model-test-result ${ok ? "success" : "error"}`;
1872
+ status.className = `model-test-result ${ok ? "result-ok" : "result-fail"}`;
1763
1873
  }
1764
1874
 
1765
1875
  if (!saveBtn.dataset.bound) {
@@ -1767,15 +1877,26 @@ const Settings = (() => {
1767
1877
  saveBtn.addEventListener("click", async () => {
1768
1878
  status.textContent = "";
1769
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
+
1770
1891
  try {
1771
1892
  const res = await fetch("/api/config/search", {
1772
1893
  method: "PATCH",
1773
1894
  headers: { "Content-Type": "application/json" },
1774
- body: JSON.stringify({ provider: select.value, key: keyInput.value })
1895
+ body: JSON.stringify({ provider: select.value, key: key })
1775
1896
  });
1776
1897
  const data = await res.json();
1777
1898
  if (data.ok) {
1778
- _showStatus(I18n.t("settings.search.saved"), true);
1899
+ Modal.toast(I18n.t("settings.search.saved"), "success");
1779
1900
  } else {
1780
1901
  _showStatus(data.error || I18n.t("settings.search.failed"), false);
1781
1902
  }
@@ -1987,6 +2108,20 @@ const Settings = (() => {
1987
2108
  badge.className = "brand-status-value badge-active";
1988
2109
  }
1989
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
+
1990
2125
  // Fetch full brand info for expiry date and support QR code
1991
2126
  fetch("/api/brand").then(r => r.json()).then(info => {
1992
2127
  const expiresEl = document.getElementById("brand-status-expires");
@@ -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);
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"
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.20
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