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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/README.md +19 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +17 -3
- data/lib/clacky/agent/message_compressor_helper.rb +36 -2
- data/lib/clacky/agent/session_serializer.rb +45 -4
- data/lib/clacky/agent.rb +135 -55
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +10 -5
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +5 -4
- data/lib/clacky/client.rb +12 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +45 -4
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +38 -138
- data/lib/clacky/search_config.rb +47 -13
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/http_server.rb +285 -53
- data/lib/clacky/server/model_prices.rb +10 -5
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +37 -7
- data/lib/clacky/server/web_ui_controller.rb +72 -0
- data/lib/clacky/session_manager.rb +28 -6
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/model_pricing.rb +242 -39
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1158 -249
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +92 -97
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/features/archive/store.js +196 -0
- data/lib/clacky/web/features/archive/view.js +409 -0
- data/lib/clacky/web/features/backup/view.js +17 -7
- data/lib/clacky/web/features/billing/view.js +84 -10
- data/lib/clacky/web/features/channels/view.js +75 -29
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/trash/store.js +14 -11
- data/lib/clacky/web/features/trash/view.js +66 -13
- data/lib/clacky/web/features/workspace/view.js +421 -16
- data/lib/clacky/web/i18n.js +242 -70
- data/lib/clacky/web/index.html +270 -117
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +609 -267
- data/lib/clacky/web/settings.js +186 -32
- data/lib/clacky/web/ws-dispatcher.js +33 -21
- data/lib/clacky.rb +4 -2
- metadata +6 -1
data/lib/clacky/web/settings.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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>` +
|
|
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 ? "
|
|
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:
|
|
1895
|
+
body: JSON.stringify({ provider: select.value, key: key })
|
|
1758
1896
|
});
|
|
1759
1897
|
const data = await res.json();
|
|
1760
1898
|
if (data.ok) {
|
|
1761
|
-
|
|
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
|
-
|
|
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.
|
|
373
|
-
//
|
|
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 || (
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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
|
-
|
|
802
|
+
append(`<span>${escapeHtml(ev.message)}</span>${action}${_buildRawDetail(ev.raw_message)}`);
|
|
791
803
|
return;
|
|
792
804
|
}
|
|
793
|
-
|
|
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.
|
|
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
|