openclacky 1.2.5 → 1.2.7
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 +43 -0
- data/README.md +34 -0
- data/README_CN.md +34 -0
- data/lib/clacky/agent/cost_tracker.rb +24 -10
- data/lib/clacky/agent/llm_caller.rb +25 -3
- data/lib/clacky/agent/message_compressor.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +6 -2
- data/lib/clacky/agent/session_serializer.rb +23 -4
- data/lib/clacky/agent/tool_executor.rb +14 -0
- data/lib/clacky/agent/tool_registry.rb +0 -7
- data/lib/clacky/agent.rb +43 -10
- data/lib/clacky/agent_config.rb +54 -6
- data/lib/clacky/billing/billing_store.rb +62 -4
- data/lib/clacky/brand_config.rb +5 -0
- data/lib/clacky/cli.rb +76 -24
- data/lib/clacky/client.rb +59 -4
- data/lib/clacky/default_parsers/wps_parser.rb +82 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +5 -2
- data/lib/clacky/message_format/anthropic.rb +13 -3
- data/lib/clacky/message_format/bedrock.rb +2 -2
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/platform_http_client.rb +28 -1
- data/lib/clacky/providers.rb +11 -29
- data/lib/clacky/server/channel/channel_manager.rb +148 -12
- data/lib/clacky/server/channel/channel_ui_controller.rb +4 -2
- data/lib/clacky/server/http_server.rb +133 -13
- data/lib/clacky/server/session_registry.rb +30 -4
- data/lib/clacky/server/web_ui_controller.rb +6 -3
- data/lib/clacky/tools/browser.rb +4 -13
- data/lib/clacky/tools/terminal.rb +23 -27
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +3 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +659 -75
- data/lib/clacky/web/app.js +0 -1
- data/lib/clacky/web/billing.js +371 -99
- data/lib/clacky/web/i18n.js +48 -2
- data/lib/clacky/web/index.html +34 -1
- data/lib/clacky/web/sessions.js +213 -82
- data/lib/clacky/web/settings.js +59 -17
- data/lib/clacky/web/workspace.js +204 -0
- data/lib/clacky/web/ws-dispatcher.js +19 -3
- data/lib/clacky.rb +9 -3
- metadata +4 -5
- data/lib/clacky/tools/list_tasks.rb +0 -54
- data/lib/clacky/tools/redo_task.rb +0 -41
- data/lib/clacky/tools/undo_task.rb +0 -35
data/lib/clacky/web/settings.js
CHANGED
|
@@ -6,6 +6,11 @@ const Settings = (() => {
|
|
|
6
6
|
let _models = [];
|
|
7
7
|
// Provider presets loaded from server
|
|
8
8
|
let _providers = [];
|
|
9
|
+
// Provider id selected in the model edit modal (null when "Custom" or unset).
|
|
10
|
+
// Used to opt the request into anthropic_format=true when the user picks
|
|
11
|
+
// the Anthropic provider; other providers leave the flag unset and let the
|
|
12
|
+
// backend's runtime inference decide.
|
|
13
|
+
let _modalSelectedProviderId = null;
|
|
9
14
|
|
|
10
15
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
11
16
|
|
|
@@ -52,9 +57,14 @@ const Settings = (() => {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
function _getProviderName(model) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
const p = _findProviderByBaseUrl(model.base_url);
|
|
61
|
+
return p ? p.name : I18n.t("settings.models.provider.custom");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _findProviderByBaseUrl(baseUrl) {
|
|
65
|
+
if (!baseUrl) return null;
|
|
66
|
+
const url = String(baseUrl).trim().replace(/\/+$/, "");
|
|
67
|
+
return _providers.find(p => {
|
|
58
68
|
const candidates = [p.base_url].concat(
|
|
59
69
|
Array.isArray(p.endpoint_variants) ? p.endpoint_variants.map(v => v.base_url) : []
|
|
60
70
|
).filter(Boolean);
|
|
@@ -62,14 +72,15 @@ const Settings = (() => {
|
|
|
62
72
|
const norm = String(c).replace(/\/+$/, "");
|
|
63
73
|
return url === norm || url.startsWith(norm + "/");
|
|
64
74
|
});
|
|
65
|
-
});
|
|
66
|
-
return provider ? provider.name : I18n.t("settings.models.provider.custom");
|
|
75
|
+
}) || null;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
function _renderCard(container, model, index) {
|
|
70
79
|
const isDefault = model.type === "default";
|
|
71
80
|
const isLite = model.type === "lite";
|
|
72
|
-
const
|
|
81
|
+
const provider = _findProviderByBaseUrl(model.base_url);
|
|
82
|
+
const providerName = provider ? provider.name : I18n.t("settings.models.provider.custom");
|
|
83
|
+
const websiteUrl = provider && provider.website_url;
|
|
73
84
|
const displayName = model.model || I18n.t("settings.models.unnamed");
|
|
74
85
|
|
|
75
86
|
const card = document.createElement("div");
|
|
@@ -84,7 +95,7 @@ const Settings = (() => {
|
|
|
84
95
|
${isLite ? `<span class="badge badge-lite">${I18n.t("settings.models.badge.lite")}</span>` : ""}
|
|
85
96
|
</div>
|
|
86
97
|
<div class="model-card-grid-provider">${_esc(providerName)}</div>
|
|
87
|
-
${model.
|
|
98
|
+
${model.api_key_masked ? `<div class="model-card-grid-model">${_esc(model.api_key_masked)}</div>` : ""}
|
|
88
99
|
<div class="model-card-grid-status">
|
|
89
100
|
<span class="model-test-result" data-index="${index}"></span>
|
|
90
101
|
</div>
|
|
@@ -107,6 +118,12 @@ const Settings = (() => {
|
|
|
107
118
|
<span>${I18n.t("settings.models.btn.delete")}</span>
|
|
108
119
|
</button>` : ""}
|
|
109
120
|
</div>
|
|
121
|
+
${websiteUrl ? `<div class="model-card-grid-footer">
|
|
122
|
+
<a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer">
|
|
123
|
+
${I18n.t("settings.models.link.topUp")}
|
|
124
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7"/><path d="M8 7h9v9"/></svg>
|
|
125
|
+
</a>
|
|
126
|
+
</div>` : ""}
|
|
110
127
|
`;
|
|
111
128
|
|
|
112
129
|
container.appendChild(card);
|
|
@@ -150,7 +167,11 @@ const Settings = (() => {
|
|
|
150
167
|
document.getElementById("model-modal-default-field").style.display = "none";
|
|
151
168
|
|
|
152
169
|
// Set provider dropdown value
|
|
153
|
-
const
|
|
170
|
+
const matched = _findProviderByBaseUrl(model.base_url);
|
|
171
|
+
// Preserve an explicit anthropic_format=true even if base_url is custom:
|
|
172
|
+
// the user may have configured a self-hosted Anthropic-compatible proxy.
|
|
173
|
+
_modalSelectedProviderId = matched ? matched.id : (model.anthropic_format ? "anthropic" : null);
|
|
174
|
+
const providerName = matched ? matched.name : I18n.t("settings.models.provider.custom");
|
|
154
175
|
const providerValue = document.getElementById("model-modal-provider-value");
|
|
155
176
|
providerValue.textContent = providerName;
|
|
156
177
|
providerValue.classList.remove("placeholder");
|
|
@@ -164,6 +185,7 @@ const Settings = (() => {
|
|
|
164
185
|
document.getElementById("model-modal-set-default").checked = true;
|
|
165
186
|
|
|
166
187
|
// Reset provider dropdown
|
|
188
|
+
_modalSelectedProviderId = null;
|
|
167
189
|
const providerValue = document.getElementById("model-modal-provider-value");
|
|
168
190
|
providerValue.textContent = I18n.t("settings.models.placeholder.provider");
|
|
169
191
|
providerValue.classList.add("placeholder");
|
|
@@ -208,6 +230,10 @@ const Settings = (() => {
|
|
|
208
230
|
const value = option.dataset.value;
|
|
209
231
|
const text = option.dataset.label || option.textContent.trim();
|
|
210
232
|
|
|
233
|
+
// Track the picked provider so test/save can flag anthropic_format=true
|
|
234
|
+
// when the user explicitly picks Anthropic. Empty / "custom" → null.
|
|
235
|
+
_modalSelectedProviderId = (value && value !== "custom") ? value : null;
|
|
236
|
+
|
|
211
237
|
const providerValue = document.getElementById("model-modal-provider-value");
|
|
212
238
|
providerValue.textContent = text;
|
|
213
239
|
providerValue.classList.toggle("placeholder", !value);
|
|
@@ -256,15 +282,18 @@ const Settings = (() => {
|
|
|
256
282
|
_showTestResult(index, null, "");
|
|
257
283
|
|
|
258
284
|
try {
|
|
285
|
+
const body = {
|
|
286
|
+
model: model.model,
|
|
287
|
+
base_url: model.base_url,
|
|
288
|
+
api_key: model.api_key_masked,
|
|
289
|
+
index
|
|
290
|
+
};
|
|
291
|
+
if (model.anthropic_format) body.anthropic_format = true;
|
|
292
|
+
|
|
259
293
|
const testRes = await fetch("/api/config/test", {
|
|
260
294
|
method: "POST",
|
|
261
295
|
headers: { "Content-Type": "application/json" },
|
|
262
|
-
body: JSON.stringify(
|
|
263
|
-
model: model.model,
|
|
264
|
-
base_url: model.base_url,
|
|
265
|
-
api_key: model.api_key_masked,
|
|
266
|
-
index
|
|
267
|
-
})
|
|
296
|
+
body: JSON.stringify(body)
|
|
268
297
|
});
|
|
269
298
|
const testData = await testRes.json();
|
|
270
299
|
_showTestResult(index, testData.ok, testData.message);
|
|
@@ -285,15 +314,25 @@ const Settings = (() => {
|
|
|
285
314
|
|
|
286
315
|
saveBtn.disabled = true;
|
|
287
316
|
|
|
317
|
+
// Anthropic protocol is opted in only when the user picks the Anthropic
|
|
318
|
+
// provider in the modal. Other providers leave the flag absent so the
|
|
319
|
+
// backend's runtime inference (provider preset + model api overrides)
|
|
320
|
+
// decides — preserving e.g. OpenRouter's per-model anthropic-messages
|
|
321
|
+
// routing for Claude sub-models.
|
|
322
|
+
const anthropic_format = _modalSelectedProviderId === "anthropic";
|
|
323
|
+
|
|
288
324
|
// Step 1: Test first
|
|
289
325
|
saveBtn.textContent = I18n.t("settings.models.btn.testing");
|
|
290
326
|
_showModalTestResult(null, "");
|
|
291
327
|
|
|
292
328
|
try {
|
|
329
|
+
const testBody = { model, base_url, api_key, index };
|
|
330
|
+
if (anthropic_format) testBody.anthropic_format = true;
|
|
331
|
+
|
|
293
332
|
const testRes = await fetch("/api/config/test", {
|
|
294
333
|
method: "POST",
|
|
295
334
|
headers: { "Content-Type": "application/json" },
|
|
296
|
-
body: JSON.stringify(
|
|
335
|
+
body: JSON.stringify(testBody)
|
|
297
336
|
});
|
|
298
337
|
const testData = await testRes.json();
|
|
299
338
|
_showModalTestResult(testData.ok, testData.message);
|
|
@@ -317,7 +356,7 @@ const Settings = (() => {
|
|
|
317
356
|
const existing = isNew ? {} : (_models[index] || {});
|
|
318
357
|
const hasId = !!existing.id;
|
|
319
358
|
|
|
320
|
-
const payload = { model, base_url, anthropic_format
|
|
359
|
+
const payload = { model, base_url, anthropic_format };
|
|
321
360
|
if (isNew) {
|
|
322
361
|
if (document.getElementById("model-modal-set-default").checked) {
|
|
323
362
|
payload.type = "default";
|
|
@@ -886,7 +925,10 @@ const Settings = (() => {
|
|
|
886
925
|
model: card.querySelector(`[data-key="model"]`).value.trim(),
|
|
887
926
|
base_url: card.querySelector(`[data-key="base_url"]`).value.trim(),
|
|
888
927
|
api_key: card.querySelector(`[data-key="api_key"]`).value.trim(),
|
|
889
|
-
|
|
928
|
+
// The inline card form has no provider picker — preserve whatever the
|
|
929
|
+
// model was saved with. The modal flow is the only place where the
|
|
930
|
+
// user can flip this flag.
|
|
931
|
+
anthropic_format: !!_models[index]?.anthropic_format,
|
|
890
932
|
type: _models[index]?.type ?? null
|
|
891
933
|
};
|
|
892
934
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Workspace panel — lazy file tree for the active session's working directory.
|
|
2
|
+
// Lists one directory level at a time via GET /api/sessions/:id/files,
|
|
3
|
+
// expands/collapses folders in place, and downloads files on click via
|
|
4
|
+
// POST /api/file-action.
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const Workspace = (() => {
|
|
8
|
+
const STORAGE_KEY = "clacky.workspace.open";
|
|
9
|
+
|
|
10
|
+
let _sessionId = null;
|
|
11
|
+
let _workingDir = null;
|
|
12
|
+
let _open = false;
|
|
13
|
+
|
|
14
|
+
const $ = (id) => document.getElementById(id);
|
|
15
|
+
const t = (key) => (typeof I18n !== "undefined" ? I18n.t(key) : key);
|
|
16
|
+
|
|
17
|
+
const ICON_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
18
|
+
const ICON_FILE = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
|
|
19
|
+
const ICON_CARET = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
20
|
+
|
|
21
|
+
function formatSize(bytes) {
|
|
22
|
+
if (bytes == null) return "";
|
|
23
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
24
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
25
|
+
let n = bytes / 1024, i = 0;
|
|
26
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
27
|
+
return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function fetchEntries(relPath) {
|
|
31
|
+
const url = `/api/sessions/${encodeURIComponent(_sessionId)}/files?path=${encodeURIComponent(relPath || "")}`;
|
|
32
|
+
const resp = await fetch(url);
|
|
33
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
34
|
+
const data = await resp.json();
|
|
35
|
+
return data.entries || [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderEntries(entries) {
|
|
39
|
+
const frag = document.createDocumentFragment();
|
|
40
|
+
if (!entries.length) {
|
|
41
|
+
const empty = document.createElement("div");
|
|
42
|
+
empty.className = "wt-empty";
|
|
43
|
+
empty.textContent = t("workspace.empty");
|
|
44
|
+
frag.appendChild(empty);
|
|
45
|
+
return frag;
|
|
46
|
+
}
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
frag.appendChild(buildNode(entry));
|
|
49
|
+
}
|
|
50
|
+
return frag;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildNode(entry) {
|
|
54
|
+
const node = document.createElement("div");
|
|
55
|
+
node.className = "wt-node";
|
|
56
|
+
|
|
57
|
+
const row = document.createElement("div");
|
|
58
|
+
row.className = "wt-row";
|
|
59
|
+
row.title = entry.name;
|
|
60
|
+
|
|
61
|
+
const caret = document.createElement("span");
|
|
62
|
+
caret.className = "wt-caret" + (entry.type === "dir" ? "" : " leaf");
|
|
63
|
+
if (entry.type === "dir") caret.innerHTML = ICON_CARET;
|
|
64
|
+
|
|
65
|
+
const icon = document.createElement("span");
|
|
66
|
+
icon.className = "wt-icon";
|
|
67
|
+
icon.innerHTML = entry.type === "dir" ? ICON_FOLDER : ICON_FILE;
|
|
68
|
+
|
|
69
|
+
const name = document.createElement("span");
|
|
70
|
+
name.className = "wt-name";
|
|
71
|
+
name.textContent = entry.name;
|
|
72
|
+
|
|
73
|
+
row.appendChild(caret);
|
|
74
|
+
row.appendChild(icon);
|
|
75
|
+
row.appendChild(name);
|
|
76
|
+
|
|
77
|
+
if (entry.type === "file") {
|
|
78
|
+
const size = document.createElement("span");
|
|
79
|
+
size.className = "wt-size";
|
|
80
|
+
size.textContent = formatSize(entry.size);
|
|
81
|
+
row.appendChild(size);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
node.appendChild(row);
|
|
85
|
+
|
|
86
|
+
if (entry.type === "dir") {
|
|
87
|
+
const children = document.createElement("div");
|
|
88
|
+
children.className = "wt-children";
|
|
89
|
+
children.style.display = "none";
|
|
90
|
+
node.appendChild(children);
|
|
91
|
+
row.addEventListener("click", () => toggleDir(entry, caret, children));
|
|
92
|
+
} else {
|
|
93
|
+
row.addEventListener("click", () => downloadFile(entry));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return node;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function toggleDir(entry, caret, children) {
|
|
100
|
+
const isOpen = caret.classList.contains("open");
|
|
101
|
+
if (isOpen) {
|
|
102
|
+
caret.classList.remove("open");
|
|
103
|
+
children.style.display = "none";
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
caret.classList.add("open");
|
|
107
|
+
children.style.display = "";
|
|
108
|
+
if (children.dataset.loaded === "1") return;
|
|
109
|
+
|
|
110
|
+
children.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
|
|
111
|
+
try {
|
|
112
|
+
const entries = await fetchEntries(entry.path);
|
|
113
|
+
children.innerHTML = "";
|
|
114
|
+
children.appendChild(renderEntries(entries));
|
|
115
|
+
children.dataset.loaded = "1";
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error("workspace load failed:", err);
|
|
118
|
+
children.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function downloadFile(entry) {
|
|
123
|
+
const fullPath = _workingDir.replace(/\/+$/, "") + "/" + entry.path;
|
|
124
|
+
try {
|
|
125
|
+
const resp = await fetch("/api/file-action", {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "Content-Type": "application/json" },
|
|
128
|
+
body: JSON.stringify({ path: fullPath, action: "download" })
|
|
129
|
+
});
|
|
130
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
131
|
+
const blob = await resp.blob();
|
|
132
|
+
const url = URL.createObjectURL(blob);
|
|
133
|
+
const a = document.createElement("a");
|
|
134
|
+
a.href = url;
|
|
135
|
+
a.download = entry.name;
|
|
136
|
+
document.body.appendChild(a);
|
|
137
|
+
a.click();
|
|
138
|
+
a.remove();
|
|
139
|
+
URL.revokeObjectURL(url);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error("download failed:", err);
|
|
142
|
+
if (typeof Modal !== "undefined") Modal.toast(t("workspace.downloadFailed"), "error");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function loadRoot() {
|
|
147
|
+
const tree = $("workspace-tree");
|
|
148
|
+
if (!tree || !_sessionId) return;
|
|
149
|
+
tree.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
|
|
150
|
+
try {
|
|
151
|
+
const entries = await fetchEntries("");
|
|
152
|
+
tree.innerHTML = "";
|
|
153
|
+
tree.appendChild(renderEntries(entries));
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error("workspace load failed:", err);
|
|
156
|
+
tree.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function applyOpenState() {
|
|
161
|
+
const panel = $("workspace-panel");
|
|
162
|
+
const opener = $("btn-workspace-open");
|
|
163
|
+
if (!panel) return;
|
|
164
|
+
const hasSession = !!_sessionId;
|
|
165
|
+
panel.classList.toggle("collapsed", !(_open && hasSession));
|
|
166
|
+
if (opener) opener.style.display = (!_open && hasSession) ? "" : "none";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function setOpen(open) {
|
|
170
|
+
_open = open;
|
|
171
|
+
try { localStorage.setItem(STORAGE_KEY, open ? "1" : "0"); } catch (_) {}
|
|
172
|
+
applyOpenState();
|
|
173
|
+
if (open) loadRoot();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
init() {
|
|
178
|
+
try { _open = localStorage.getItem(STORAGE_KEY) === "1"; } catch (_) { _open = false; }
|
|
179
|
+
|
|
180
|
+
const close = $("btn-workspace-close");
|
|
181
|
+
const opener = $("btn-workspace-open");
|
|
182
|
+
const refresh = $("btn-workspace-refresh");
|
|
183
|
+
if (close) close.addEventListener("click", () => setOpen(false));
|
|
184
|
+
if (opener) opener.addEventListener("click", () => setOpen(true));
|
|
185
|
+
if (refresh) refresh.addEventListener("click", () => loadRoot());
|
|
186
|
+
|
|
187
|
+
applyOpenState();
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Called from Sessions.updateInfoBar whenever the active session changes.
|
|
191
|
+
onSession(session) {
|
|
192
|
+
const newId = session ? session.id : null;
|
|
193
|
+
const newDir = session ? session.working_dir : null;
|
|
194
|
+
const changed = newId !== _sessionId || newDir !== _workingDir;
|
|
195
|
+
_sessionId = newId;
|
|
196
|
+
_workingDir = newDir;
|
|
197
|
+
applyOpenState();
|
|
198
|
+
if (changed && _open && _sessionId) loadRoot();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
})();
|
|
202
|
+
|
|
203
|
+
document.addEventListener("DOMContentLoaded", () => Workspace.init());
|
|
204
|
+
window.Workspace = Workspace;
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
WS.onEvent(ev => {
|
|
21
|
-
console.log("[DEBUG] WS event received:", ev.type, ev);
|
|
22
21
|
switch (ev.type) {
|
|
23
22
|
|
|
24
23
|
// ── Internal WS lifecycle ──────────────────────────────────────────
|
|
@@ -273,11 +272,28 @@ WS.onEvent(ev => {
|
|
|
273
272
|
break;
|
|
274
273
|
|
|
275
274
|
case "error":
|
|
276
|
-
if (!ev.session_id || ev.session_id === Sessions.activeId)
|
|
277
|
-
|
|
275
|
+
if (!ev.session_id || ev.session_id === Sessions.activeId) {
|
|
276
|
+
renderErrorEvent(ev);
|
|
277
|
+
}
|
|
278
278
|
break;
|
|
279
279
|
}
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
+
// ── Error rendering ────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
function renderErrorEvent(ev) {
|
|
285
|
+
if (ev.code === "insufficient_credit") {
|
|
286
|
+
const body = escapeHtml(I18n.t("error.insufficient_credit"));
|
|
287
|
+
const action = ev.top_up_url
|
|
288
|
+
? ` <a href="${escapeHtml(ev.top_up_url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(I18n.t("error.insufficient_credit.action"))} →</a>`
|
|
289
|
+
: "";
|
|
290
|
+
Sessions.appendMsg("error", `<span>${body}${action}</span>`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
Sessions.appendMsg("error", escapeHtml(ev.message));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
window.renderErrorEvent = renderErrorEvent;
|
|
297
|
+
|
|
282
298
|
|
|
283
299
|
})();
|
data/lib/clacky.rb
CHANGED
|
@@ -118,9 +118,6 @@ require_relative "clacky/tools/todo_manager"
|
|
|
118
118
|
require_relative "clacky/tools/trash_manager"
|
|
119
119
|
require_relative "clacky/tools/request_user_feedback"
|
|
120
120
|
require_relative "clacky/tools/invoke_skill"
|
|
121
|
-
require_relative "clacky/tools/undo_task"
|
|
122
|
-
require_relative "clacky/tools/redo_task"
|
|
123
|
-
require_relative "clacky/tools/list_tasks"
|
|
124
121
|
require_relative "clacky/tools/browser"
|
|
125
122
|
require_relative "clacky/tools/terminal"
|
|
126
123
|
require_relative "clacky/mcp/client"
|
|
@@ -139,6 +136,15 @@ module Clacky
|
|
|
139
136
|
class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
|
|
140
137
|
class AgentError < StandardError; end
|
|
141
138
|
class BadRequestError < AgentError; end # 400 errors — our request was malformed, history should be rolled back
|
|
139
|
+
class InsufficientCreditError < AgentError
|
|
140
|
+
attr_reader :error_code, :provider_id
|
|
141
|
+
|
|
142
|
+
def initialize(message, error_code: nil, provider_id: nil)
|
|
143
|
+
super(message)
|
|
144
|
+
@error_code = error_code
|
|
145
|
+
@provider_id = provider_id
|
|
146
|
+
end
|
|
147
|
+
end
|
|
142
148
|
class RetryableError < StandardError; end # Transient errors that should be retried (5xx, HTML response, rate limit)
|
|
143
149
|
# Upstream (model/router like OpenRouter/Bedrock) returned finish_reason="stop" together with
|
|
144
150
|
# one or more tool_calls whose `arguments` JSON was truncated (empty, "{}" placeholder, or
|
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.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -360,6 +360,7 @@ files:
|
|
|
360
360
|
- lib/clacky/default_parsers/pdf_parser_ocr.py
|
|
361
361
|
- lib/clacky/default_parsers/pdf_parser_plumber.py
|
|
362
362
|
- lib/clacky/default_parsers/pptx_parser.rb
|
|
363
|
+
- lib/clacky/default_parsers/wps_parser.rb
|
|
363
364
|
- lib/clacky/default_parsers/xlsx_parser.rb
|
|
364
365
|
- lib/clacky/default_skills/browser-setup/SKILL.md
|
|
365
366
|
- lib/clacky/default_skills/channel-manager/SKILL.md
|
|
@@ -461,8 +462,6 @@ files:
|
|
|
461
462
|
- lib/clacky/tools/glob.rb
|
|
462
463
|
- lib/clacky/tools/grep.rb
|
|
463
464
|
- lib/clacky/tools/invoke_skill.rb
|
|
464
|
-
- lib/clacky/tools/list_tasks.rb
|
|
465
|
-
- lib/clacky/tools/redo_task.rb
|
|
466
465
|
- lib/clacky/tools/request_user_feedback.rb
|
|
467
466
|
- lib/clacky/tools/security.rb
|
|
468
467
|
- lib/clacky/tools/terminal.rb
|
|
@@ -472,7 +471,6 @@ files:
|
|
|
472
471
|
- lib/clacky/tools/terminal/session_manager.rb
|
|
473
472
|
- lib/clacky/tools/todo_manager.rb
|
|
474
473
|
- lib/clacky/tools/trash_manager.rb
|
|
475
|
-
- lib/clacky/tools/undo_task.rb
|
|
476
474
|
- lib/clacky/tools/web_fetch.rb
|
|
477
475
|
- lib/clacky/tools/web_search.rb
|
|
478
476
|
- lib/clacky/tools/write.rb
|
|
@@ -572,6 +570,7 @@ files:
|
|
|
572
570
|
- lib/clacky/web/vendor/katex/katex.min.js
|
|
573
571
|
- lib/clacky/web/version.js
|
|
574
572
|
- lib/clacky/web/weixin-qr.html
|
|
573
|
+
- lib/clacky/web/workspace.js
|
|
575
574
|
- lib/clacky/web/ws-dispatcher.js
|
|
576
575
|
- lib/clacky/web/ws.js
|
|
577
576
|
- scripts/build/build.sh
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Clacky
|
|
4
|
-
module Tools
|
|
5
|
-
# Tool for listing task history (Time Machine feature)
|
|
6
|
-
class ListTasks < Base
|
|
7
|
-
self.tool_name = "list_tasks"
|
|
8
|
-
self.tool_description = "List recent tasks in the task history with summaries. " \
|
|
9
|
-
"Shows current task, past tasks, and future tasks (after undo). " \
|
|
10
|
-
"Use when user wants to see task history or choose which task to undo/redo to."
|
|
11
|
-
self.tool_category = "time_machine"
|
|
12
|
-
self.tool_parameters = {
|
|
13
|
-
type: "object",
|
|
14
|
-
properties: {
|
|
15
|
-
limit: {
|
|
16
|
-
type: "integer",
|
|
17
|
-
description: "Maximum number of recent tasks to show (default: 10)",
|
|
18
|
-
default: 10
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
def execute(agent:, limit: 10, **_args)
|
|
24
|
-
history = agent.get_task_history(limit: limit)
|
|
25
|
-
|
|
26
|
-
if history.empty?
|
|
27
|
-
return "No task history available."
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
lines = ["Task History:"]
|
|
31
|
-
history.each do |task|
|
|
32
|
-
indicator = case task[:status]
|
|
33
|
-
when :current then "→"
|
|
34
|
-
when :past then " "
|
|
35
|
-
when :future then "↯"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
branch_indicator = task[:has_branches] ? " ⎇" : ""
|
|
39
|
-
lines << "#{indicator}#{branch_indicator} Task #{task[:task_id]}: #{task[:summary]}"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
lines.join("\n")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def format_call(limit: 10, **_args)
|
|
46
|
-
"Listing task history (limit: #{limit})..."
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def format_result(result)
|
|
50
|
-
result
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Clacky
|
|
4
|
-
module Tools
|
|
5
|
-
# Tool for redoing a task after undo (Time Machine feature)
|
|
6
|
-
class RedoTask < Base
|
|
7
|
-
self.tool_name = "redo_task"
|
|
8
|
-
self.tool_description = "Redo to a specific task after undo. Restores files to that task's state. " \
|
|
9
|
-
"Use when user wants to go forward to a future task or switch to a different branch."
|
|
10
|
-
self.tool_category = "time_machine"
|
|
11
|
-
self.tool_parameters = {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: {
|
|
14
|
-
task_id: {
|
|
15
|
-
type: "integer",
|
|
16
|
-
description: "The task ID to redo to (must be greater than current active task)"
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
required: ["task_id"]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
def execute(agent:, task_id:, **_args)
|
|
23
|
-
result = agent.switch_to_task(task_id)
|
|
24
|
-
|
|
25
|
-
if result[:success]
|
|
26
|
-
result[:message]
|
|
27
|
-
else
|
|
28
|
-
"Error: #{result[:message]}"
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def format_call(task_id:, **_args)
|
|
33
|
-
"Redoing to task #{task_id}..."
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def format_result(result)
|
|
37
|
-
result
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Clacky
|
|
4
|
-
module Tools
|
|
5
|
-
# Tool for undoing the last task (Time Machine feature)
|
|
6
|
-
class UndoTask < Base
|
|
7
|
-
self.tool_name = "undo_task"
|
|
8
|
-
self.tool_description = "Undo the last task and restore files to previous state. " \
|
|
9
|
-
"Use when user wants to go back to previous state or undo recent changes."
|
|
10
|
-
self.tool_category = "time_machine"
|
|
11
|
-
self.tool_parameters = {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: {}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
def execute(agent:, **_args)
|
|
17
|
-
result = agent.undo_last_task
|
|
18
|
-
|
|
19
|
-
if result[:success]
|
|
20
|
-
result[:message]
|
|
21
|
-
else
|
|
22
|
-
"Error: #{result[:message]}"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def format_call(**_args)
|
|
27
|
-
"Undoing last task..."
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def format_result(result)
|
|
31
|
-
result
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|