openclacky 1.3.3 → 1.3.4
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 +26 -0
- data/docs/rich_ui_guide.md +277 -0
- data/docs/rich_ui_refactor_plan.md +396 -0
- data/lib/clacky/agent/llm_caller.rb +10 -4
- data/lib/clacky/agent/session_serializer.rb +3 -2
- data/lib/clacky/agent.rb +3 -2
- data/lib/clacky/agent_config.rb +2 -14
- data/lib/clacky/api_extension.rb +262 -0
- data/lib/clacky/api_extension_loader.rb +156 -0
- data/lib/clacky/cli.rb +93 -3
- data/lib/clacky/client.rb +38 -13
- data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +1 -1
- data/lib/clacky/default_skills/media-gen/SKILL.md +9 -6
- data/lib/clacky/idle_compression_timer.rb +3 -1
- data/lib/clacky/locales/en.rb +26 -0
- data/lib/clacky/locales/i18n.rb +26 -0
- data/lib/clacky/locales/zh.rb +26 -0
- data/lib/clacky/rich_ui/components/base_component.rb +50 -0
- data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
- data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
- data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
- data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
- data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
- data/lib/clacky/rich_ui/components/status_view.rb +58 -0
- data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
- data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
- data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
- data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
- data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
- data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
- data/lib/clacky/rich_ui/view_renderer.rb +291 -0
- data/lib/clacky/rich_ui.rb +57 -0
- data/lib/clacky/rich_ui_controller.rb +3 -1549
- data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
- data/lib/clacky/server/http_server.rb +150 -103
- data/lib/clacky/server/session_registry.rb +1 -1
- data/lib/clacky/shell_hook_loader.rb +1 -1
- data/lib/clacky/tools/edit.rb +14 -2
- data/lib/clacky/ui2/ui_controller.rb +7 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +56 -59
- data/lib/clacky/web/app.js +65 -7
- data/lib/clacky/web/components/onboard.js +18 -2
- data/lib/clacky/web/core/aside.js +8 -3
- data/lib/clacky/web/core/ext.js +1 -1
- data/lib/clacky/web/features/skills/store.js +30 -2
- data/lib/clacky/web/features/skills/view.js +32 -1
- data/lib/clacky/web/features/workspace/view.js +1 -1
- data/lib/clacky/web/i18n.js +32 -20
- data/lib/clacky/web/index.html +9 -17
- data/lib/clacky/web/sessions.js +286 -28
- data/lib/clacky/web/settings.js +109 -111
- data/lib/clacky/web/ws-dispatcher.js +7 -3
- data/lib/clacky.rb +17 -2
- metadata +38 -2
- data/lib/clacky/media/output_dir.rb +0 -43
data/lib/clacky/web/settings.js
CHANGED
|
@@ -17,7 +17,6 @@ const Settings = (() => {
|
|
|
17
17
|
function open() {
|
|
18
18
|
_load();
|
|
19
19
|
_loadMedia();
|
|
20
|
-
_initMediaOutputDir();
|
|
21
20
|
_loadBrand();
|
|
22
21
|
_loadBrowserStatus();
|
|
23
22
|
_initNetworkSettings();
|
|
@@ -118,6 +117,10 @@ const Settings = (() => {
|
|
|
118
117
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
119
118
|
<span>${I18n.t("settings.models.btn.edit")}</span>
|
|
120
119
|
</button>
|
|
120
|
+
<button class="btn-card-grid-action" data-index="${index}" data-action="duplicate">
|
|
121
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
122
|
+
<span>${I18n.t("settings.models.btn.duplicate")}</span>
|
|
123
|
+
</button>
|
|
121
124
|
${_models.length > 1 ? `<button class="btn-card-grid-action btn-card-grid-action-danger" data-index="${index}" data-action="delete">
|
|
122
125
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
123
126
|
<span>${I18n.t("settings.models.btn.delete")}</span>
|
|
@@ -141,10 +144,11 @@ const Settings = (() => {
|
|
|
141
144
|
btn.addEventListener("click", () => {
|
|
142
145
|
const action = btn.dataset.action;
|
|
143
146
|
switch (action) {
|
|
144
|
-
case "edit":
|
|
145
|
-
case "test":
|
|
146
|
-
case "delete":
|
|
147
|
-
case "default":
|
|
147
|
+
case "edit": _openModal(index); break;
|
|
148
|
+
case "test": _testModel(index); break;
|
|
149
|
+
case "delete": _removeModel(index); break;
|
|
150
|
+
case "default": _setAsDefault(index); break;
|
|
151
|
+
case "duplicate": _openModalDuplicate(index); break;
|
|
148
152
|
}
|
|
149
153
|
});
|
|
150
154
|
});
|
|
@@ -158,6 +162,7 @@ const Settings = (() => {
|
|
|
158
162
|
const indexInput = document.getElementById("model-modal-index");
|
|
159
163
|
|
|
160
164
|
indexInput.value = index;
|
|
165
|
+
document.getElementById("model-modal-source-id").value = "";
|
|
161
166
|
|
|
162
167
|
// Populate provider dropdown
|
|
163
168
|
_populateModalProviderDropdown();
|
|
@@ -233,6 +238,30 @@ const Settings = (() => {
|
|
|
233
238
|
document.body.style.overflow = "";
|
|
234
239
|
}
|
|
235
240
|
|
|
241
|
+
function _openModalDuplicate(index) {
|
|
242
|
+
const source = _models[index];
|
|
243
|
+
if (!source) return;
|
|
244
|
+
|
|
245
|
+
_openModal(-1);
|
|
246
|
+
|
|
247
|
+
document.getElementById("model-modal-source-id").value = source.id || "";
|
|
248
|
+
document.getElementById("model-modal-title").textContent = I18n.t("settings.models.modal.duplicate");
|
|
249
|
+
document.getElementById("model-modal-model").value = source.model || "";
|
|
250
|
+
document.getElementById("model-modal-baseurl").value = source.base_url || "";
|
|
251
|
+
document.getElementById("model-modal-apikey").value = source.api_key_masked || "";
|
|
252
|
+
|
|
253
|
+
const matched = _findProviderByBaseUrl(source.base_url);
|
|
254
|
+
_modalSelectedProviderId = matched ? matched.id : (source.anthropic_format ? "anthropic" : null);
|
|
255
|
+
const providerValue = document.getElementById("model-modal-provider-value");
|
|
256
|
+
if (matched) {
|
|
257
|
+
providerValue.textContent = matched.name;
|
|
258
|
+
providerValue.classList.remove("placeholder");
|
|
259
|
+
|
|
260
|
+
const promoHint = document.getElementById("model-modal-promo-hint");
|
|
261
|
+
if (matched.id !== "openclacky") promoHint.classList.remove("visible");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
236
265
|
function _populateModalProviderDropdown() {
|
|
237
266
|
const dropdown = document.getElementById("model-modal-provider-dropdown");
|
|
238
267
|
dropdown.innerHTML = `
|
|
@@ -334,13 +363,14 @@ const Settings = (() => {
|
|
|
334
363
|
const isNew = index < 0;
|
|
335
364
|
const existing = isNew ? {} : (_models[index] || {});
|
|
336
365
|
const existingId = existing.id || null;
|
|
366
|
+
const sourceId = document.getElementById("model-modal-source-id").value || null;
|
|
337
367
|
|
|
338
368
|
// Step 1: Test first
|
|
339
369
|
saveBtn.textContent = I18n.t("settings.models.btn.testing");
|
|
340
370
|
_showModalTestResult(null, "");
|
|
341
371
|
|
|
342
372
|
const result = await ModelTester.testConnection({
|
|
343
|
-
model, base_url, api_key, index, id: existingId, anthropic_format
|
|
373
|
+
model, base_url, api_key, index, id: existingId || sourceId, anthropic_format
|
|
344
374
|
});
|
|
345
375
|
|
|
346
376
|
if (result.rewrote) {
|
|
@@ -376,10 +406,14 @@ const Settings = (() => {
|
|
|
376
406
|
}
|
|
377
407
|
|
|
378
408
|
if (!hasId && !payload.api_key) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
409
|
+
if (sourceId) {
|
|
410
|
+
payload.source_id = sourceId;
|
|
411
|
+
} else {
|
|
412
|
+
saveBtn.textContent = I18n.t("settings.models.btn.save");
|
|
413
|
+
saveBtn.disabled = false;
|
|
414
|
+
_showModalTestResult(false, I18n.t("settings.models.placeholder.apikey"));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
383
417
|
}
|
|
384
418
|
|
|
385
419
|
const saveResult = await ModelTester.saveModel(payload, { existingId: hasId ? existingId : null });
|
|
@@ -406,6 +440,25 @@ const Settings = (() => {
|
|
|
406
440
|
el.className = `model-test-result ${ok ? "result-ok" : "result-fail"}`;
|
|
407
441
|
}
|
|
408
442
|
|
|
443
|
+
function _positionDropdownFixed(dropdown, anchor) {
|
|
444
|
+
const rect = anchor.getBoundingClientRect();
|
|
445
|
+
dropdown.style.position = "fixed";
|
|
446
|
+
dropdown.style.top = (rect.bottom + 4) + "px";
|
|
447
|
+
dropdown.style.left = rect.left + "px";
|
|
448
|
+
dropdown.style.width = rect.width + "px";
|
|
449
|
+
dropdown.style.right = "auto";
|
|
450
|
+
dropdown.style.zIndex = "9999";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function _resetDropdownPosition(dropdown) {
|
|
454
|
+
dropdown.style.position = "";
|
|
455
|
+
dropdown.style.top = "";
|
|
456
|
+
dropdown.style.left = "";
|
|
457
|
+
dropdown.style.width = "";
|
|
458
|
+
dropdown.style.right = "";
|
|
459
|
+
dropdown.style.zIndex = "";
|
|
460
|
+
}
|
|
461
|
+
|
|
409
462
|
function _initModal() {
|
|
410
463
|
// Close button
|
|
411
464
|
document.getElementById("model-modal-close").addEventListener("click", _closeModal);
|
|
@@ -459,34 +512,58 @@ const Settings = (() => {
|
|
|
459
512
|
// Model dropdown functionality
|
|
460
513
|
const modelDropdownBtn = document.getElementById("model-modal-model-dropdown-btn");
|
|
461
514
|
const modelDropdown = document.getElementById("model-modal-model-dropdown");
|
|
515
|
+
const modelCombobox = document.getElementById("model-modal-model-combobox");
|
|
462
516
|
const modelInput = document.getElementById("model-modal-model");
|
|
463
517
|
|
|
518
|
+
function _openModelDropdown() {
|
|
519
|
+
_closeBaseUrlDropdown();
|
|
520
|
+
_updateModalModelDropdown();
|
|
521
|
+
_positionDropdownFixed(modelDropdown, modelCombobox);
|
|
522
|
+
modelDropdown.style.display = "block";
|
|
523
|
+
document.body.appendChild(modelDropdown);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function _closeModelDropdown() {
|
|
527
|
+
modelDropdown.style.display = "none";
|
|
528
|
+
_resetDropdownPosition(modelDropdown);
|
|
529
|
+
modelCombobox.appendChild(modelDropdown);
|
|
530
|
+
}
|
|
531
|
+
|
|
464
532
|
modelDropdownBtn.addEventListener("click", (e) => {
|
|
465
533
|
e.stopPropagation();
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (!isOpen) {
|
|
471
|
-
_updateModalModelDropdown();
|
|
472
|
-
modelDropdown.style.display = "block";
|
|
534
|
+
if (modelDropdown.style.display === "block") {
|
|
535
|
+
_closeModelDropdown();
|
|
536
|
+
} else {
|
|
537
|
+
_openModelDropdown();
|
|
473
538
|
}
|
|
474
539
|
});
|
|
475
540
|
|
|
476
541
|
// Base URL dropdown functionality
|
|
477
542
|
const baseUrlDropdownBtn = document.getElementById("model-modal-baseurl-dropdown-btn");
|
|
478
543
|
const baseUrlDropdown = document.getElementById("model-modal-baseurl-dropdown");
|
|
544
|
+
const baseUrlCombobox = document.getElementById("model-modal-baseurl-combobox");
|
|
479
545
|
const baseUrlInput = document.getElementById("model-modal-baseurl");
|
|
480
546
|
|
|
547
|
+
function _openBaseUrlDropdown() {
|
|
548
|
+
_closeModelDropdown();
|
|
549
|
+
_updateModalBaseUrlDropdown();
|
|
550
|
+
_positionDropdownFixed(baseUrlDropdown, baseUrlCombobox);
|
|
551
|
+
baseUrlDropdown.style.display = "block";
|
|
552
|
+
document.body.appendChild(baseUrlDropdown);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function _closeBaseUrlDropdown() {
|
|
556
|
+
baseUrlDropdown.style.display = "none";
|
|
557
|
+
_resetDropdownPosition(baseUrlDropdown);
|
|
558
|
+
baseUrlCombobox.appendChild(baseUrlDropdown);
|
|
559
|
+
}
|
|
560
|
+
|
|
481
561
|
baseUrlDropdownBtn.addEventListener("click", (e) => {
|
|
482
562
|
e.stopPropagation();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (!isOpen) {
|
|
488
|
-
_updateModalBaseUrlDropdown();
|
|
489
|
-
baseUrlDropdown.style.display = "block";
|
|
563
|
+
if (baseUrlDropdown.style.display === "block") {
|
|
564
|
+
_closeBaseUrlDropdown();
|
|
565
|
+
} else {
|
|
566
|
+
_openBaseUrlDropdown();
|
|
490
567
|
}
|
|
491
568
|
});
|
|
492
569
|
|
|
@@ -495,10 +572,14 @@ const Settings = (() => {
|
|
|
495
572
|
_updateModalModelDropdown();
|
|
496
573
|
});
|
|
497
574
|
|
|
498
|
-
// Close
|
|
499
|
-
document.addEventListener("
|
|
500
|
-
modelDropdown.
|
|
501
|
-
|
|
575
|
+
// Close dropdowns on outside click
|
|
576
|
+
document.addEventListener("mousedown", (e) => {
|
|
577
|
+
if (!modelCombobox.contains(e.target) && !modelDropdown.contains(e.target)) {
|
|
578
|
+
_closeModelDropdown();
|
|
579
|
+
}
|
|
580
|
+
if (!baseUrlCombobox.contains(e.target) && !baseUrlDropdown.contains(e.target)) {
|
|
581
|
+
_closeBaseUrlDropdown();
|
|
582
|
+
}
|
|
502
583
|
});
|
|
503
584
|
}
|
|
504
585
|
|
|
@@ -1272,89 +1353,6 @@ const Settings = (() => {
|
|
|
1272
1353
|
}
|
|
1273
1354
|
}
|
|
1274
1355
|
|
|
1275
|
-
// ── Media output directory ────────────────────────────────────────────────────
|
|
1276
|
-
//
|
|
1277
|
-
// Single user-facing override for where /api/media/* writes generated files.
|
|
1278
|
-
// Mirrors the proxy_url section above (same field-input + save/clear pair)
|
|
1279
|
-
// because the data shape is identical (one optional string). Resolution
|
|
1280
|
-
// priority lives server-side in Clacky::Media::OutputDir.resolve:
|
|
1281
|
-
// per-call output_dir → media_output_dir (this setting) → default_working_dir → Dir.pwd
|
|
1282
|
-
|
|
1283
|
-
async function _initMediaOutputDir() {
|
|
1284
|
-
const input = document.getElementById("settings-media-output-dir");
|
|
1285
|
-
const browseBtn = document.getElementById("btn-browse-media-output-dir");
|
|
1286
|
-
const clearBtn = document.getElementById("btn-clear-media-output-dir");
|
|
1287
|
-
const status = document.getElementById("settings-media-output-dir-status");
|
|
1288
|
-
if (!input || !browseBtn) return;
|
|
1289
|
-
|
|
1290
|
-
try {
|
|
1291
|
-
const res = await fetch("/api/config/media-output-dir");
|
|
1292
|
-
const data = await res.json();
|
|
1293
|
-
if (data.ok) {
|
|
1294
|
-
input.value = data.value || "";
|
|
1295
|
-
// Show the system fallback as a placeholder hint so the user sees
|
|
1296
|
-
// where files would land if they leave the field blank.
|
|
1297
|
-
if (data.default) input.placeholder = data.default;
|
|
1298
|
-
}
|
|
1299
|
-
} catch (_) { /* non-critical */ }
|
|
1300
|
-
|
|
1301
|
-
async function _patchMediaOutputDir(value, successKey) {
|
|
1302
|
-
status.textContent = "";
|
|
1303
|
-
status.className = "model-test-result";
|
|
1304
|
-
try {
|
|
1305
|
-
const res = await fetch("/api/config/media-output-dir", {
|
|
1306
|
-
method: "PATCH",
|
|
1307
|
-
headers: { "Content-Type": "application/json" },
|
|
1308
|
-
body: JSON.stringify({ value: value })
|
|
1309
|
-
});
|
|
1310
|
-
const data = await res.json();
|
|
1311
|
-
if (data.ok) {
|
|
1312
|
-
status.textContent = I18n.t(successKey);
|
|
1313
|
-
status.className = "model-test-result success";
|
|
1314
|
-
// Auto-hide the toast after a short delay so it doesn't linger
|
|
1315
|
-
// forever (looks like a stuck banner). 2s is enough to read.
|
|
1316
|
-
clearTimeout(_patchMediaOutputDir._hideTimer);
|
|
1317
|
-
_patchMediaOutputDir._hideTimer = setTimeout(() => {
|
|
1318
|
-
status.textContent = "";
|
|
1319
|
-
status.className = "model-test-result";
|
|
1320
|
-
}, 2000);
|
|
1321
|
-
// Server may have expanded `~` or normalized the path — reflect
|
|
1322
|
-
// the canonical value back into the input so the user sees what
|
|
1323
|
-
// was actually persisted.
|
|
1324
|
-
input.value = data.value || "";
|
|
1325
|
-
} else {
|
|
1326
|
-
status.textContent = data.error || I18n.t("settings.media.output_dir.invalid");
|
|
1327
|
-
status.className = "model-test-result error";
|
|
1328
|
-
}
|
|
1329
|
-
} catch (e) {
|
|
1330
|
-
status.textContent = e.message || I18n.t("settings.media.output_dir.invalid");
|
|
1331
|
-
status.className = "model-test-result error";
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
if (!browseBtn.dataset.bound) {
|
|
1336
|
-
browseBtn.dataset.bound = "1";
|
|
1337
|
-
browseBtn.addEventListener("click", async () => {
|
|
1338
|
-
// Reuse the global directory picker in session-less mode (browses
|
|
1339
|
-
// the real filesystem via /api/dirs). Picker resolves to an absolute
|
|
1340
|
-
// path on confirm, or null on cancel.
|
|
1341
|
-
const start = (input.value || "").trim();
|
|
1342
|
-
const picked = await window.openDirectoryPicker(start, null, I18n.t("settings.media.output_dir.picker"));
|
|
1343
|
-
if (!picked) return;
|
|
1344
|
-
// Persist immediately — no separate Save click needed.
|
|
1345
|
-
await _patchMediaOutputDir(picked, "settings.media.output_dir.saved");
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
if (clearBtn && !clearBtn.dataset.bound) {
|
|
1350
|
-
clearBtn.dataset.bound = "1";
|
|
1351
|
-
clearBtn.addEventListener("click", () => {
|
|
1352
|
-
input.value = "";
|
|
1353
|
-
_patchMediaOutputDir("", "settings.media.output_dir.cleared");
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
1356
|
// ── Brand & License ───────────────────────────────────────────────────────────
|
|
1359
1357
|
|
|
1360
1358
|
// Whether the server was started with --brand-test (relaxed key validation).
|
|
@@ -217,7 +217,7 @@ WS.onEvent(ev => {
|
|
|
217
217
|
const pendingMsg = Sessions.takePendingMessage();
|
|
218
218
|
if (pendingMsg && pendingMsg.session_id === ev.session_id) {
|
|
219
219
|
Sessions.appendMsg("user", escapeHtml(pendingMsg.content), { time: new Date() });
|
|
220
|
-
WS.send({ type: "message", session_id: pendingMsg.session_id, content: pendingMsg.content });
|
|
220
|
+
WS.send({ type: "message", session_id: pendingMsg.session_id, content: pendingMsg.content, lang: I18n.lang() });
|
|
221
221
|
}
|
|
222
222
|
break;
|
|
223
223
|
}
|
|
@@ -298,8 +298,12 @@ WS.onEvent(ev => {
|
|
|
298
298
|
|
|
299
299
|
// ── Chat messages ──────────────────────────────────────────────────
|
|
300
300
|
case "history_user_message":
|
|
301
|
-
// Emitted
|
|
302
|
-
//
|
|
301
|
+
// Emitted by show_user_message before agent.run; stamp the authoritative
|
|
302
|
+
// created_at onto the optimistically-rendered bubble and register it in
|
|
303
|
+
// the dedup set so the subsequent history fetch skips this round.
|
|
304
|
+
if (ev.session_id === Sessions.activeId && ev.created_at) {
|
|
305
|
+
Sessions.stampLastUserBubble(ev.created_at);
|
|
306
|
+
}
|
|
303
307
|
break;
|
|
304
308
|
|
|
305
309
|
case "assistant_message":
|
data/lib/clacky.rb
CHANGED
|
@@ -68,6 +68,7 @@ require_relative "clacky/message_format/bedrock"
|
|
|
68
68
|
require_relative "clacky/bedrock_stream_aggregator"
|
|
69
69
|
require_relative "clacky/openai_stream_aggregator"
|
|
70
70
|
require_relative "clacky/anthropic_stream_aggregator"
|
|
71
|
+
require_relative "clacky/locales/i18n"
|
|
71
72
|
require_relative "clacky/client"
|
|
72
73
|
require_relative "clacky/skill"
|
|
73
74
|
require_relative "clacky/skill_loader"
|
|
@@ -128,7 +129,6 @@ require_relative "clacky/mcp/registry"
|
|
|
128
129
|
require_relative "clacky/mcp/skill_provider"
|
|
129
130
|
require_relative "clacky/media/base"
|
|
130
131
|
require_relative "clacky/media/openai_compat"
|
|
131
|
-
require_relative "clacky/media/output_dir"
|
|
132
132
|
require_relative "clacky/media/generator"
|
|
133
133
|
require_relative "clacky/vision/resolver"
|
|
134
134
|
require_relative "clacky/telemetry"
|
|
@@ -145,10 +145,25 @@ require_relative "clacky/cli"
|
|
|
145
145
|
require_relative "clacky/patch_loader"
|
|
146
146
|
Clacky::PatchLoader.load_all
|
|
147
147
|
|
|
148
|
+
# HTTP API extension layer: define the base class + loader + dispatcher.
|
|
149
|
+
# Loading of user extensions is deferred to HttpServer#start so the host
|
|
150
|
+
# process is fully initialized before extension handlers can resolve helpers
|
|
151
|
+
# like session_manager or agent_config.
|
|
152
|
+
require_relative "clacky/api_extension"
|
|
153
|
+
require_relative "clacky/api_extension_loader"
|
|
154
|
+
require_relative "clacky/server/api_extension_dispatcher"
|
|
155
|
+
|
|
148
156
|
module Clacky
|
|
149
157
|
class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
|
|
150
158
|
class AgentError < StandardError; end
|
|
151
|
-
class BadRequestError < AgentError
|
|
159
|
+
class BadRequestError < AgentError
|
|
160
|
+
attr_reader :display_message
|
|
161
|
+
|
|
162
|
+
def initialize(message, display_message: nil)
|
|
163
|
+
super(message)
|
|
164
|
+
@display_message = display_message
|
|
165
|
+
end
|
|
166
|
+
end
|
|
152
167
|
class InsufficientCreditError < AgentError
|
|
153
168
|
attr_reader :error_code, :provider_id
|
|
154
169
|
|
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.3.
|
|
4
|
+
version: 1.3.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
@@ -245,6 +245,20 @@ dependencies:
|
|
|
245
245
|
- - "<"
|
|
246
246
|
- !ruby/object:Gem::Version
|
|
247
247
|
version: '5.0'
|
|
248
|
+
- !ruby/object:Gem::Dependency
|
|
249
|
+
name: ruby_rich
|
|
250
|
+
requirement: !ruby/object:Gem::Requirement
|
|
251
|
+
requirements:
|
|
252
|
+
- - "~>"
|
|
253
|
+
- !ruby/object:Gem::Version
|
|
254
|
+
version: 0.5.2
|
|
255
|
+
type: :runtime
|
|
256
|
+
prerelease: false
|
|
257
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
258
|
+
requirements:
|
|
259
|
+
- - "~>"
|
|
260
|
+
- !ruby/object:Gem::Version
|
|
261
|
+
version: 0.5.2
|
|
248
262
|
- !ruby/object:Gem::Dependency
|
|
249
263
|
name: chunky_png
|
|
250
264
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -313,6 +327,8 @@ files:
|
|
|
313
327
|
- docs/engineering-article.md
|
|
314
328
|
- docs/mcp-architecture.md
|
|
315
329
|
- docs/mcp.example.json
|
|
330
|
+
- docs/rich_ui_guide.md
|
|
331
|
+
- docs/rich_ui_refactor_plan.md
|
|
316
332
|
- docs/session-skill-invocation.md
|
|
317
333
|
- docs/time_machine_design.md
|
|
318
334
|
- docs/ui2-architecture.md
|
|
@@ -339,6 +355,8 @@ files:
|
|
|
339
355
|
- lib/clacky/agent_config.rb
|
|
340
356
|
- lib/clacky/agent_profile.rb
|
|
341
357
|
- lib/clacky/anthropic_stream_aggregator.rb
|
|
358
|
+
- lib/clacky/api_extension.rb
|
|
359
|
+
- lib/clacky/api_extension_loader.rb
|
|
342
360
|
- lib/clacky/banner.rb
|
|
343
361
|
- lib/clacky/bedrock_stream_aggregator.rb
|
|
344
362
|
- lib/clacky/billing/billing_record.rb
|
|
@@ -412,6 +430,9 @@ files:
|
|
|
412
430
|
- lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb
|
|
413
431
|
- lib/clacky/idle_compression_timer.rb
|
|
414
432
|
- lib/clacky/json_ui_controller.rb
|
|
433
|
+
- lib/clacky/locales/en.rb
|
|
434
|
+
- lib/clacky/locales/i18n.rb
|
|
435
|
+
- lib/clacky/locales/zh.rb
|
|
415
436
|
- lib/clacky/mcp/client.rb
|
|
416
437
|
- lib/clacky/mcp/http_transport.rb
|
|
417
438
|
- lib/clacky/mcp/registry.rb
|
|
@@ -424,7 +445,6 @@ files:
|
|
|
424
445
|
- lib/clacky/media/gemini.rb
|
|
425
446
|
- lib/clacky/media/generator.rb
|
|
426
447
|
- lib/clacky/media/openai_compat.rb
|
|
427
|
-
- lib/clacky/media/output_dir.rb
|
|
428
448
|
- lib/clacky/message_format/anthropic.rb
|
|
429
449
|
- lib/clacky/message_format/bedrock.rb
|
|
430
450
|
- lib/clacky/message_format/open_ai.rb
|
|
@@ -435,7 +455,23 @@ files:
|
|
|
435
455
|
- lib/clacky/platform_http_client.rb
|
|
436
456
|
- lib/clacky/providers.rb
|
|
437
457
|
- lib/clacky/proxy_config.rb
|
|
458
|
+
- lib/clacky/rich_ui.rb
|
|
459
|
+
- lib/clacky/rich_ui/components/base_component.rb
|
|
460
|
+
- lib/clacky/rich_ui/components/dialogs/approval_dialog.rb
|
|
461
|
+
- lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb
|
|
462
|
+
- lib/clacky/rich_ui/components/dialogs/form_dialog.rb
|
|
463
|
+
- lib/clacky/rich_ui/components/sidebar.rb
|
|
464
|
+
- lib/clacky/rich_ui/components/sidebar_panels.rb
|
|
465
|
+
- lib/clacky/rich_ui/components/status_view.rb
|
|
466
|
+
- lib/clacky/rich_ui/components/thinking_live_view.rb
|
|
467
|
+
- lib/clacky/rich_ui/entry_tracker.rb
|
|
468
|
+
- lib/clacky/rich_ui/layout_adapter.rb
|
|
469
|
+
- lib/clacky/rich_ui/progress_handle_adapter.rb
|
|
470
|
+
- lib/clacky/rich_ui/rich_ui_controller.rb
|
|
471
|
+
- lib/clacky/rich_ui/shell/rich_agent_shell.rb
|
|
472
|
+
- lib/clacky/rich_ui/view_renderer.rb
|
|
438
473
|
- lib/clacky/rich_ui_controller.rb
|
|
474
|
+
- lib/clacky/server/api_extension_dispatcher.rb
|
|
439
475
|
- lib/clacky/server/backup_manager.rb
|
|
440
476
|
- lib/clacky/server/browser_manager.rb
|
|
441
477
|
- lib/clacky/server/channel.rb
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Clacky
|
|
4
|
-
module Media
|
|
5
|
-
# Resolves the on-disk root for generated media files (images, videos,
|
|
6
|
-
# audio) according to a fixed precedence:
|
|
7
|
-
#
|
|
8
|
-
# 1. `param` — explicit `output_dir` from the API caller.
|
|
9
|
-
# Highest priority; lets a single call land
|
|
10
|
-
# somewhere specific (e.g. a doc's project root).
|
|
11
|
-
# 2. `configured` — user setting from AgentConfig#media_output_dir.
|
|
12
|
-
# Set via Settings → Models → Media Output Directory.
|
|
13
|
-
# 3. `fallback` — process default; preserves legacy behavior for
|
|
14
|
-
# configs that have neither key set.
|
|
15
|
-
#
|
|
16
|
-
# Pure function on purpose: callers (HTTP handlers) read the configured
|
|
17
|
-
# value off AgentConfig and inject it here. Keeps this helper trivially
|
|
18
|
-
# unit-testable and free of global state.
|
|
19
|
-
#
|
|
20
|
-
# The final on-disk path is `<resolved>/assets/generated/<file>` —
|
|
21
|
-
# the `assets/generated/` suffix is appended by Media::Base#save_*
|
|
22
|
-
# for stable relative-path semantics across markdown / slide outputs,
|
|
23
|
-
# and is intentionally not configurable here.
|
|
24
|
-
module OutputDir
|
|
25
|
-
# @param param [String, nil] explicit per-call override
|
|
26
|
-
# @param configured [String, nil] user-configured default
|
|
27
|
-
# @param fallback [String] last-resort default (defaults to Dir.pwd)
|
|
28
|
-
# @return [String] absolute or `~`-prefixed path; the
|
|
29
|
-
# caller's File.join with "assets/generated/" handles `~` via the
|
|
30
|
-
# surrounding FileUtils.mkdir_p call only when expanded — for safety
|
|
31
|
-
# we expand `~` here so downstream sees an absolute path.
|
|
32
|
-
def self.resolve(param:, configured:, fallback: Dir.pwd)
|
|
33
|
-
chosen = first_present(param, configured) || fallback
|
|
34
|
-
File.expand_path(chosen.to_s)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# @api private
|
|
38
|
-
def self.first_present(*candidates)
|
|
39
|
-
candidates.find { |c| c.is_a?(String) && !c.strip.empty? }
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|