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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +18 -0
- 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 +1 -0
- data/lib/clacky/agent/message_compressor_helper.rb +28 -2
- data/lib/clacky/agent/session_serializer.rb +20 -2
- data/lib/clacky/agent.rb +56 -43
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +5 -1
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +3 -2
- data/lib/clacky/client.rb +5 -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 +21 -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 +21 -3
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +23 -14
- 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 +194 -47
- data/lib/clacky/server/model_prices.rb +5 -3
- 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 +19 -7
- data/lib/clacky/server/web_ui_controller.rb +61 -38
- data/lib/clacky/session_manager.rb +7 -1
- 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 +132 -13
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +366 -105
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +3 -1
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/features/archive/store.js +30 -16
- data/lib/clacky/web/features/archive/view.js +20 -10
- data/lib/clacky/web/features/backup/view.js +5 -5
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/trash/store.js +10 -7
- data/lib/clacky/web/features/trash/view.js +9 -4
- data/lib/clacky/web/i18n.js +106 -30
- data/lib/clacky/web/index.html +90 -60
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +244 -131
- data/lib/clacky/web/settings.js +143 -8
- data/lib/clacky/web/ws-dispatcher.js +9 -13
- data/lib/clacky.rb +1 -0
- metadata +4 -1
data/lib/clacky/web/settings.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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>` +
|
|
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 ? "
|
|
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:
|
|
1895
|
+
body: JSON.stringify({ provider: select.value, key: key })
|
|
1775
1896
|
});
|
|
1776
1897
|
const data = await res.json();
|
|
1777
1898
|
if (data.ok) {
|
|
1778
|
-
|
|
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.
|
|
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);
|
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.
|
|
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
|