openclacky 1.2.13 → 1.2.15
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/.clacky/skills/gem-release/SKILL.md +4 -0
- data/CHANGELOG.md +28 -0
- data/lib/clacky/agent/session_serializer.rb +1 -0
- data/lib/clacky/agent.rb +123 -14
- data/lib/clacky/agent_config.rb +136 -10
- data/lib/clacky/client.rb +59 -46
- data/lib/clacky/default_parsers/pdf_parser.rb +70 -86
- data/lib/clacky/default_parsers/pdf_parser_vlm.py +136 -0
- data/lib/clacky/providers.rb +37 -0
- data/lib/clacky/proxy_config.rb +65 -0
- data/lib/clacky/server/http_server.rb +202 -5
- data/lib/clacky/server/scheduler.rb +13 -10
- data/lib/clacky/ui2/progress_handle.rb +17 -13
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/vision/resolver.rb +157 -0
- data/lib/clacky/web/app.css +56 -6
- data/lib/clacky/web/i18n.js +24 -2
- data/lib/clacky/web/index.html +21 -0
- data/lib/clacky/web/notify.js +154 -0
- data/lib/clacky/web/notify.mp3 +0 -0
- data/lib/clacky/web/settings.js +88 -12
- data/lib/clacky/web/ws-dispatcher.js +8 -0
- data/lib/clacky.rb +4 -0
- metadata +7 -2
data/lib/clacky/web/index.html
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
46
|
<div id="header-right">
|
|
47
|
+
<button id="notify-toggle-header" class="theme-toggle-btn" data-i18n-title="notify.tooltip.off" title="Sound on task complete"></button>
|
|
47
48
|
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme">
|
|
48
49
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
49
50
|
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
|
|
@@ -869,6 +870,25 @@
|
|
|
869
870
|
</div>
|
|
870
871
|
</section>
|
|
871
872
|
|
|
873
|
+
<!-- Network / Proxy section -->
|
|
874
|
+
<section class="settings-section" id="network-section">
|
|
875
|
+
<div class="settings-section-title">
|
|
876
|
+
<span data-i18n="settings.network.title">Network</span>
|
|
877
|
+
</div>
|
|
878
|
+
<div class="settings-network">
|
|
879
|
+
<p class="settings-network-desc" data-i18n="settings.network.desc">Clacky always ignores HTTP_PROXY / HTTPS_PROXY from your shell. To route Clacky's outbound traffic through a proxy, set an explicit URL below.</p>
|
|
880
|
+
<div class="settings-network-url">
|
|
881
|
+
<label class="settings-network-url-label" for="settings-proxy-url" data-i18n="settings.network.proxyUrl">Proxy URL</label>
|
|
882
|
+
<div class="settings-network-url-row">
|
|
883
|
+
<input type="text" id="settings-proxy-url" class="field-input" placeholder="http://user:pass@host:port" autocomplete="off" spellcheck="false">
|
|
884
|
+
<button type="button" id="btn-save-proxy-url" class="btn-settings-action" data-i18n="settings.network.save">Save</button>
|
|
885
|
+
<button type="button" id="btn-clear-proxy-url" class="btn-settings-action" data-i18n="settings.network.clear">Clear</button>
|
|
886
|
+
</div>
|
|
887
|
+
<div id="settings-proxy-url-status" class="model-test-result"></div>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
</section>
|
|
891
|
+
|
|
872
892
|
<!-- Brand & License section -->
|
|
873
893
|
<section class="settings-section" id="brand-license-section">
|
|
874
894
|
<div class="settings-section-title">
|
|
@@ -1300,6 +1320,7 @@
|
|
|
1300
1320
|
<script src="/i18n.js"></script>
|
|
1301
1321
|
<script src="/auth.js"></script>
|
|
1302
1322
|
<script src="/theme.js"></script>
|
|
1323
|
+
<script src="/notify.js"></script>
|
|
1303
1324
|
<script src="/ws.js"></script>
|
|
1304
1325
|
<script src="/ws-dispatcher.js"></script>
|
|
1305
1326
|
<script src="/sessions.js"></script>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// notify.js — Task-complete sound notification module
|
|
2
|
+
//
|
|
3
|
+
// Plays a short sound when an agent task finishes, driven by the global
|
|
4
|
+
// `task_finished` event the server broadcasts (broadcast_all) the moment a
|
|
5
|
+
// task completes. We listen to this dedicated signal — rather than `complete`
|
|
6
|
+
// (only delivered to subscribers of that session) — so a background session
|
|
7
|
+
// finishing still reaches every browser. Whether a task *finished* is decided
|
|
8
|
+
// on the backend; this module only decides whether the user is looking.
|
|
9
|
+
//
|
|
10
|
+
// The chime only fires when the user is NOT actively looking at the
|
|
11
|
+
// finished session. "Not looking" means ANY of:
|
|
12
|
+
// 1. The finished session is not the currently open one
|
|
13
|
+
// (sid !== Sessions.activeId)
|
|
14
|
+
// 2. The browser window has lost focus (!document.hasFocus())
|
|
15
|
+
// 3. The tab is hidden / minimised / behind another tab (document.hidden)
|
|
16
|
+
//
|
|
17
|
+
// If the user is focused on the very session that just finished, we stay
|
|
18
|
+
// silent — they can already see the result.
|
|
19
|
+
//
|
|
20
|
+
// The feature is gated behind a header toggle (🔔/🔕) next to the theme
|
|
21
|
+
// switcher. Default OFF; the choice is persisted to localStorage.
|
|
22
|
+
//
|
|
23
|
+
// No history replay: a chime is a live cue, never re-fired on page refresh.
|
|
24
|
+
// The audio file is served as a static asset (/notify.mp3) by WEBrick.
|
|
25
|
+
//
|
|
26
|
+
// Depends on: Sessions (sessions.js) for activeId, I18n (i18n.js) for the
|
|
27
|
+
// tooltip text. Both are optional — guarded with typeof checks.
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
29
|
+
const Notify = (() => {
|
|
30
|
+
const STORAGE_KEY = "clacky-notify-sound";
|
|
31
|
+
const AUDIO_SRC = "/notify.mp3";
|
|
32
|
+
|
|
33
|
+
let _audio = null;
|
|
34
|
+
|
|
35
|
+
// ── State ────────────────────────────────────────────────────────────
|
|
36
|
+
// Default OFF: only enabled when localStorage explicitly says "on".
|
|
37
|
+
function enabled() {
|
|
38
|
+
return localStorage.getItem(STORAGE_KEY) === "on";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setEnabled(on) {
|
|
42
|
+
localStorage.setItem(STORAGE_KEY, on ? "on" : "off");
|
|
43
|
+
_updateToggleIcon();
|
|
44
|
+
// On enabling, "prime" the audio element within this user gesture so the
|
|
45
|
+
// browser's autoplay policy lets later programmatic play() calls through.
|
|
46
|
+
if (on) _prime();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toggle() {
|
|
50
|
+
setEnabled(!enabled());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Audio ────────────────────────────────────────────────────────────
|
|
54
|
+
function _ensureAudio() {
|
|
55
|
+
if (!_audio) {
|
|
56
|
+
_audio = new Audio(AUDIO_SRC);
|
|
57
|
+
_audio.preload = "auto";
|
|
58
|
+
}
|
|
59
|
+
return _audio;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Play+pause+reset muted once, triggered by the toggle click (a user
|
|
63
|
+
// gesture), to satisfy autoplay policies for subsequent unmuted plays.
|
|
64
|
+
function _prime() {
|
|
65
|
+
const a = _ensureAudio();
|
|
66
|
+
const prevMuted = a.muted;
|
|
67
|
+
a.muted = true;
|
|
68
|
+
const p = a.play();
|
|
69
|
+
if (p && typeof p.then === "function") {
|
|
70
|
+
p.then(() => {
|
|
71
|
+
a.pause();
|
|
72
|
+
a.currentTime = 0;
|
|
73
|
+
a.muted = prevMuted;
|
|
74
|
+
}).catch(() => { a.muted = prevMuted; });
|
|
75
|
+
} else {
|
|
76
|
+
a.muted = prevMuted;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _play() {
|
|
81
|
+
const a = _ensureAudio();
|
|
82
|
+
try {
|
|
83
|
+
a.currentTime = 0;
|
|
84
|
+
const p = a.play();
|
|
85
|
+
// Swallow autoplay-policy rejections silently — better to miss a
|
|
86
|
+
// chime than to throw an unhandled promise rejection.
|
|
87
|
+
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
88
|
+
} catch (_e) { /* ignore */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Trigger decision ───────────────────────────────────────────────────
|
|
92
|
+
// Returns true when the user is NOT actively viewing the given session.
|
|
93
|
+
function _userIsAway(sid) {
|
|
94
|
+
// 1. Finished session is not the one currently open.
|
|
95
|
+
const activeId = (typeof Sessions !== "undefined") ? Sessions.activeId : null;
|
|
96
|
+
if (sid && sid !== activeId) return true;
|
|
97
|
+
// 2. Browser window is not focused (e.g. another app / window on top).
|
|
98
|
+
if (typeof document.hasFocus === "function" && !document.hasFocus()) return true;
|
|
99
|
+
// 3. Tab is hidden (switched to another tab, or window minimised).
|
|
100
|
+
if (document.hidden) return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Called from ws-dispatcher on the `task_finished` event — a transient global
|
|
105
|
+
// signal the server broadcasts to every client the moment an agent task
|
|
106
|
+
// completes. We only decide whether the user is looking at that session;
|
|
107
|
+
// the "did a task just finish" judgement lives on the backend.
|
|
108
|
+
function onTaskFinished(sid) {
|
|
109
|
+
if (!enabled()) return;
|
|
110
|
+
if (!_userIsAway(sid)) return;
|
|
111
|
+
_play();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Toggle button UI ───────────────────────────────────────────────────
|
|
115
|
+
function _updateToggleIcon() {
|
|
116
|
+
const btn = document.getElementById("notify-toggle-header");
|
|
117
|
+
if (!btn) return;
|
|
118
|
+
const on = enabled();
|
|
119
|
+
// Bell when ON, bell-off (muted) when OFF.
|
|
120
|
+
btn.innerHTML = on
|
|
121
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
122
|
+
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
|
|
123
|
+
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
|
|
124
|
+
</svg>`
|
|
125
|
+
: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
126
|
+
<path d="M8.7 3A6 6 0 0 1 18 8c0 1.5.2 2.8.5 3.9"/>
|
|
127
|
+
<path d="M17 17H3s3-2 3-9a4.67 4.67 0 0 1 .3-1.7"/>
|
|
128
|
+
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
|
|
129
|
+
<line x1="2" y1="2" x2="22" y2="22"/>
|
|
130
|
+
</svg>`;
|
|
131
|
+
btn.classList.toggle("notify-on", on);
|
|
132
|
+
if (typeof I18n !== "undefined") {
|
|
133
|
+
const tip = I18n.t(on ? "notify.tooltip.on" : "notify.tooltip.off");
|
|
134
|
+
btn.title = tip;
|
|
135
|
+
btn.setAttribute("aria-label", tip);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Init ─────────────────────────────────────────────────────────────
|
|
140
|
+
function init() {
|
|
141
|
+
_updateToggleIcon();
|
|
142
|
+
const btn = document.getElementById("notify-toggle-header");
|
|
143
|
+
if (btn) btn.addEventListener("click", toggle);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { init, toggle, enabled, setEnabled, onTaskFinished };
|
|
147
|
+
})();
|
|
148
|
+
|
|
149
|
+
// Initialize on load (button binding + initial icon state).
|
|
150
|
+
if (document.readyState === "loading") {
|
|
151
|
+
document.addEventListener("DOMContentLoaded", () => Notify.init());
|
|
152
|
+
} else {
|
|
153
|
+
Notify.init();
|
|
154
|
+
}
|
|
Binary file
|
data/lib/clacky/web/settings.js
CHANGED
|
@@ -19,6 +19,7 @@ const Settings = (() => {
|
|
|
19
19
|
_loadMedia();
|
|
20
20
|
_loadBrand();
|
|
21
21
|
_loadBrowserStatus();
|
|
22
|
+
_initNetworkSettings();
|
|
22
23
|
_applyAboutTabVisibility();
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -1200,6 +1201,62 @@ const Settings = (() => {
|
|
|
1200
1201
|
}
|
|
1201
1202
|
}
|
|
1202
1203
|
|
|
1204
|
+
// ── Network / Proxy ───────────────────────────────────────────────────────────
|
|
1205
|
+
|
|
1206
|
+
async function _initNetworkSettings() {
|
|
1207
|
+
const urlInput = document.getElementById("settings-proxy-url");
|
|
1208
|
+
const saveBtn = document.getElementById("btn-save-proxy-url");
|
|
1209
|
+
const clearBtn = document.getElementById("btn-clear-proxy-url");
|
|
1210
|
+
const status = document.getElementById("settings-proxy-url-status");
|
|
1211
|
+
if (!urlInput || !saveBtn) return;
|
|
1212
|
+
|
|
1213
|
+
try {
|
|
1214
|
+
const res = await fetch("/api/config/settings");
|
|
1215
|
+
const data = await res.json();
|
|
1216
|
+
if (data.ok) {
|
|
1217
|
+
urlInput.value = data.proxy_url || "";
|
|
1218
|
+
}
|
|
1219
|
+
} catch (_) { /* non-critical */ }
|
|
1220
|
+
|
|
1221
|
+
async function _patchProxyUrl(value, successKey) {
|
|
1222
|
+
status.textContent = "";
|
|
1223
|
+
status.className = "model-test-result";
|
|
1224
|
+
try {
|
|
1225
|
+
const res = await fetch("/api/config/settings", {
|
|
1226
|
+
method: "PATCH",
|
|
1227
|
+
headers: { "Content-Type": "application/json" },
|
|
1228
|
+
body: JSON.stringify({ proxy_url: value })
|
|
1229
|
+
});
|
|
1230
|
+
const data = await res.json();
|
|
1231
|
+
if (data.ok) {
|
|
1232
|
+
status.textContent = I18n.t(successKey);
|
|
1233
|
+
status.className = "model-test-result success";
|
|
1234
|
+
} else {
|
|
1235
|
+
status.textContent = data.error || I18n.t("settings.network.invalidUrl");
|
|
1236
|
+
status.className = "model-test-result error";
|
|
1237
|
+
}
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
status.textContent = e.message || I18n.t("settings.network.invalidUrl");
|
|
1240
|
+
status.className = "model-test-result error";
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (!saveBtn.dataset.bound) {
|
|
1245
|
+
saveBtn.dataset.bound = "1";
|
|
1246
|
+
saveBtn.addEventListener("click", () => {
|
|
1247
|
+
_patchProxyUrl(urlInput.value.trim(), "settings.network.saved");
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (clearBtn && !clearBtn.dataset.bound) {
|
|
1252
|
+
clearBtn.dataset.bound = "1";
|
|
1253
|
+
clearBtn.addEventListener("click", () => {
|
|
1254
|
+
urlInput.value = "";
|
|
1255
|
+
_patchProxyUrl("", "settings.network.cleared");
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1203
1260
|
// ── Brand & License ───────────────────────────────────────────────────────────
|
|
1204
1261
|
|
|
1205
1262
|
// Whether the server was started with --brand-test (relaxed key validation).
|
|
@@ -1528,7 +1585,7 @@ const Settings = (() => {
|
|
|
1528
1585
|
// The state object per kind:
|
|
1529
1586
|
// { source, configured, model, base_url, api_key_masked, provider, available }
|
|
1530
1587
|
|
|
1531
|
-
const MEDIA_KINDS = ["image", "video", "audio"];
|
|
1588
|
+
const MEDIA_KINDS = ["image", "video", "audio", "ocr"];
|
|
1532
1589
|
let _mediaState = null;
|
|
1533
1590
|
let _mediaDefaults = null;
|
|
1534
1591
|
const _mediaCustomDraft = {};
|
|
@@ -1538,10 +1595,16 @@ const Settings = (() => {
|
|
|
1538
1595
|
if (!container) return;
|
|
1539
1596
|
container.innerHTML = `<div class="settings-loading">${I18n.t("settings.media.loading")}</div>`;
|
|
1540
1597
|
try {
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1598
|
+
const [mediaRes, ocrRes] = await Promise.all([
|
|
1599
|
+
fetch("/api/config/media"),
|
|
1600
|
+
fetch("/api/config/ocr")
|
|
1601
|
+
]);
|
|
1602
|
+
const mediaData = await mediaRes.json();
|
|
1603
|
+
const ocrData = await ocrRes.json();
|
|
1604
|
+
_mediaState = mediaData.media || {};
|
|
1605
|
+
_mediaDefaults = mediaData.default_provider || {};
|
|
1606
|
+
_mediaState["ocr"] = ocrData.ocr || { source: "off", available: [] };
|
|
1607
|
+
_mediaDefaults["ocr"] = ocrData.default_provider || { available: [] };
|
|
1545
1608
|
_renderMediaRows();
|
|
1546
1609
|
} catch (e) {
|
|
1547
1610
|
container.innerHTML = `<div class="settings-error">${I18n.t("settings.media.error", { msg: e.message })}</div>`;
|
|
@@ -1557,6 +1620,14 @@ const Settings = (() => {
|
|
|
1557
1620
|
});
|
|
1558
1621
|
}
|
|
1559
1622
|
|
|
1623
|
+
function _refreshKindRows(_kind) {
|
|
1624
|
+
_renderMediaRows();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
async function _reloadKind(_kind) {
|
|
1628
|
+
await _loadMedia();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1560
1631
|
function _renderMediaRow(kind) {
|
|
1561
1632
|
const state = (_mediaState && _mediaState[kind]) || { source: "off", available: [] };
|
|
1562
1633
|
const def = (_mediaDefaults && _mediaDefaults[kind]) || { available: [] };
|
|
@@ -1662,7 +1733,7 @@ const Settings = (() => {
|
|
|
1662
1733
|
_setMediaResult(kind, "testing", I18n.t("settings.media.action.saving"));
|
|
1663
1734
|
try {
|
|
1664
1735
|
await _saveMediaConfig(kind, payload);
|
|
1665
|
-
await
|
|
1736
|
+
await _reloadKind(kind);
|
|
1666
1737
|
} catch (e) {
|
|
1667
1738
|
sel.disabled = false;
|
|
1668
1739
|
_setMediaResult(kind, "fail", e.message);
|
|
@@ -1742,7 +1813,7 @@ const Settings = (() => {
|
|
|
1742
1813
|
base_url: state.base_url || "",
|
|
1743
1814
|
api_key: ""
|
|
1744
1815
|
};
|
|
1745
|
-
|
|
1816
|
+
_refreshKindRows(kind);
|
|
1746
1817
|
});
|
|
1747
1818
|
|
|
1748
1819
|
const testBtn = document.createElement("button");
|
|
@@ -1818,7 +1889,7 @@ const Settings = (() => {
|
|
|
1818
1889
|
const fallback = (_mediaDefaults && _mediaDefaults[kind] && _mediaDefaults[kind].model) ? "auto" : "off";
|
|
1819
1890
|
_mediaState[kind] = { ..._mediaState[kind], source: fallback };
|
|
1820
1891
|
}
|
|
1821
|
-
|
|
1892
|
+
_refreshKindRows(kind);
|
|
1822
1893
|
});
|
|
1823
1894
|
|
|
1824
1895
|
const saveBtn = document.createElement("button");
|
|
@@ -1838,7 +1909,7 @@ const Settings = (() => {
|
|
|
1838
1909
|
api_key: d.api_key || ""
|
|
1839
1910
|
});
|
|
1840
1911
|
delete _mediaCustomDraft[kind];
|
|
1841
|
-
await
|
|
1912
|
+
await _reloadKind(kind);
|
|
1842
1913
|
} catch (e) {
|
|
1843
1914
|
saveBtn.disabled = false;
|
|
1844
1915
|
cancelBtn.disabled = false;
|
|
@@ -1906,7 +1977,8 @@ const Settings = (() => {
|
|
|
1906
1977
|
}
|
|
1907
1978
|
|
|
1908
1979
|
async function _saveMediaConfig(kind, body) {
|
|
1909
|
-
const
|
|
1980
|
+
const url = kind === "ocr" ? `/api/config/ocr` : `/api/config/media/${kind}`;
|
|
1981
|
+
const res = await fetch(url, {
|
|
1910
1982
|
method: "PATCH",
|
|
1911
1983
|
headers: { "Content-Type": "application/json" },
|
|
1912
1984
|
body: JSON.stringify(body)
|
|
@@ -1920,10 +1992,14 @@ const Settings = (() => {
|
|
|
1920
1992
|
|
|
1921
1993
|
async function _testMediaConfig(kind, { model, base_url, api_key }) {
|
|
1922
1994
|
try {
|
|
1923
|
-
const
|
|
1995
|
+
const url = kind === "ocr" ? `/api/config/ocr/test` : `/api/config/media/test`;
|
|
1996
|
+
const payload = kind === "ocr"
|
|
1997
|
+
? { model, base_url, api_key }
|
|
1998
|
+
: { kind, model, base_url, api_key };
|
|
1999
|
+
const res = await fetch(url, {
|
|
1924
2000
|
method: "POST",
|
|
1925
2001
|
headers: { "Content-Type": "application/json" },
|
|
1926
|
-
body: JSON.stringify(
|
|
2002
|
+
body: JSON.stringify(payload)
|
|
1927
2003
|
});
|
|
1928
2004
|
const data = await res.json().catch(() => ({}));
|
|
1929
2005
|
if (!res.ok) return { ok: false, message: data.error || `HTTP ${res.status}` };
|
|
@@ -265,6 +265,14 @@ WS.onEvent(ev => {
|
|
|
265
265
|
break;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// Transient global signal emitted the moment any agent task finishes
|
|
269
|
+
// (broadcast to every client, not just session subscribers). Used only
|
|
270
|
+
// to play the optional completion chime; the toggle gates it and the
|
|
271
|
+
// module decides whether the user is looking at that session.
|
|
272
|
+
case "task_finished":
|
|
273
|
+
if (typeof Notify !== "undefined") Notify.onTaskFinished(ev.session_id);
|
|
274
|
+
break;
|
|
275
|
+
|
|
268
276
|
case "session_renamed": {
|
|
269
277
|
Sessions.patch(ev.session_id, { name: ev.name });
|
|
270
278
|
Sessions.renderList();
|
data/lib/clacky.rb
CHANGED
|
@@ -92,6 +92,7 @@ require_relative "clacky/ui2/progress_indicator"
|
|
|
92
92
|
|
|
93
93
|
# Utils
|
|
94
94
|
require_relative "clacky/utils/logger"
|
|
95
|
+
require_relative "clacky/proxy_config"
|
|
95
96
|
require_relative "clacky/platform_http_client"
|
|
96
97
|
require_relative "clacky/utils/encoding"
|
|
97
98
|
require_relative "clacky/utils/environment_detector"
|
|
@@ -128,6 +129,7 @@ require_relative "clacky/mcp/skill_provider"
|
|
|
128
129
|
require_relative "clacky/media/base"
|
|
129
130
|
require_relative "clacky/media/openai_compat"
|
|
130
131
|
require_relative "clacky/media/generator"
|
|
132
|
+
require_relative "clacky/vision/resolver"
|
|
131
133
|
require_relative "clacky/telemetry"
|
|
132
134
|
require_relative "clacky/agent"
|
|
133
135
|
|
|
@@ -164,3 +166,5 @@ module Clacky
|
|
|
164
166
|
class BrowserNotReachableError < AgentError; end # Chrome/Edge not running or remote debugging disabled
|
|
165
167
|
# BrowserManager singleton: Clacky::BrowserManager.instance
|
|
166
168
|
end
|
|
169
|
+
|
|
170
|
+
Clacky::ProxyConfig.install!
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -359,6 +359,7 @@ files:
|
|
|
359
359
|
- lib/clacky/default_parsers/pdf_parser.rb
|
|
360
360
|
- lib/clacky/default_parsers/pdf_parser_ocr.py
|
|
361
361
|
- lib/clacky/default_parsers/pdf_parser_plumber.py
|
|
362
|
+
- lib/clacky/default_parsers/pdf_parser_vlm.py
|
|
362
363
|
- lib/clacky/default_parsers/pptx_parser.rb
|
|
363
364
|
- lib/clacky/default_parsers/wps_parser.rb
|
|
364
365
|
- lib/clacky/default_parsers/xlsx_parser.rb
|
|
@@ -428,6 +429,7 @@ files:
|
|
|
428
429
|
- lib/clacky/plain_ui_controller.rb
|
|
429
430
|
- lib/clacky/platform_http_client.rb
|
|
430
431
|
- lib/clacky/providers.rb
|
|
432
|
+
- lib/clacky/proxy_config.rb
|
|
431
433
|
- lib/clacky/rich_ui_controller.rb
|
|
432
434
|
- lib/clacky/server/browser_manager.rb
|
|
433
435
|
- lib/clacky/server/channel.rb
|
|
@@ -532,6 +534,7 @@ files:
|
|
|
532
534
|
- lib/clacky/utils/trash_directory.rb
|
|
533
535
|
- lib/clacky/utils/workspace_rules.rb
|
|
534
536
|
- lib/clacky/version.rb
|
|
537
|
+
- lib/clacky/vision/resolver.rb
|
|
535
538
|
- lib/clacky/web/app.css
|
|
536
539
|
- lib/clacky/web/app.js
|
|
537
540
|
- lib/clacky/web/apple-touch-icon-180.png
|
|
@@ -550,6 +553,8 @@ files:
|
|
|
550
553
|
- lib/clacky/web/marked.min.js
|
|
551
554
|
- lib/clacky/web/mcp.js
|
|
552
555
|
- lib/clacky/web/model-tester.js
|
|
556
|
+
- lib/clacky/web/notify.js
|
|
557
|
+
- lib/clacky/web/notify.mp3
|
|
553
558
|
- lib/clacky/web/onboard.js
|
|
554
559
|
- lib/clacky/web/profile.js
|
|
555
560
|
- lib/clacky/web/sessions.js
|