openclacky 1.3.3 → 1.3.5
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 +53 -0
- data/docs/rich_ui_guide.md +277 -0
- data/docs/rich_ui_refactor_plan.md +396 -0
- data/lib/clacky/agent/fake_tool_call_detector.rb +52 -0
- data/lib/clacky/agent/llm_caller.rb +10 -4
- data/lib/clacky/agent/session_serializer.rb +5 -3
- data/lib/clacky/agent/tool_executor.rb +0 -12
- data/lib/clacky/agent.rb +77 -11
- data/lib/clacky/agent_config.rb +2 -14
- data/lib/clacky/api_extension.rb +343 -0
- data/lib/clacky/api_extension_loader.rb +168 -0
- data/lib/clacky/cli.rb +93 -3
- data/lib/clacky/client.rb +47 -25
- data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +23 -1
- data/lib/clacky/default_agents/base_prompt.md +1 -0
- data/lib/clacky/default_extensions/meeting/handler.rb +331 -0
- data/lib/clacky/default_extensions/meeting/meeting.js +790 -0
- data/lib/clacky/default_extensions/meeting/meta.yml +3 -0
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +44 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +72 -6
- data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +114 -0
- data/lib/clacky/idle_compression_timer.rb +3 -1
- data/lib/clacky/json_ui_controller.rb +1 -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/media/base.rb +60 -0
- data/lib/clacky/media/dashscope.rb +385 -21
- data/lib/clacky/media/gemini.rb +9 -0
- data/lib/clacky/media/generator.rb +52 -0
- data/lib/clacky/media/openai_compat.rb +166 -0
- data/lib/clacky/null_ui_controller.rb +13 -0
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/providers.rb +50 -2
- 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/channel/channel_ui_controller.rb +1 -1
- data/lib/clacky/server/http_server.rb +291 -109
- data/lib/clacky/server/session_registry.rb +5 -3
- data/lib/clacky/server/web_ui_controller.rb +3 -2
- data/lib/clacky/shell_hook_loader.rb +1 -1
- data/lib/clacky/skill_loader.rb +14 -2
- data/lib/clacky/tools/edit.rb +14 -2
- data/lib/clacky/tools/terminal/output_cleaner.rb +1 -3
- data/lib/clacky/tools/terminal.rb +0 -43
- data/lib/clacky/ui2/components/modal_component.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +147 -31
- data/lib/clacky/ui_interface.rb +10 -1
- data/lib/clacky/utils/encoding.rb +25 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +201 -81
- data/lib/clacky/web/app.js +65 -7
- data/lib/clacky/web/components/onboard.js +19 -16
- data/lib/clacky/web/core/aside.js +8 -3
- data/lib/clacky/web/core/ext.js +1 -1
- data/lib/clacky/web/features/brand/view.js +8 -5
- data/lib/clacky/web/features/channels/store.js +1 -20
- data/lib/clacky/web/features/mcp/store.js +1 -20
- data/lib/clacky/web/features/profile/store.js +1 -13
- data/lib/clacky/web/features/profile/view.js +16 -4
- data/lib/clacky/web/features/skills/store.js +36 -23
- data/lib/clacky/web/features/skills/view.js +32 -1
- data/lib/clacky/web/features/version/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +1 -1
- data/lib/clacky/web/i18n.js +48 -13
- data/lib/clacky/web/index.html +24 -17
- data/lib/clacky/web/sessions.js +427 -79
- data/lib/clacky/web/settings.js +143 -113
- data/lib/clacky/web/ws-dispatcher.js +18 -6
- data/lib/clacky.rb +27 -5
- metadata +45 -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).
|
|
@@ -1510,6 +1508,7 @@ const Settings = (() => {
|
|
|
1510
1508
|
_showBrandResult(true, I18n.t("settings.brand.activated", { name: data.product_name || "configured" }));
|
|
1511
1509
|
// Apply brand name and logo across the entire UI immediately
|
|
1512
1510
|
if (data.product_name) Brand.applyBrandName(data.product_name);
|
|
1511
|
+
if (data.theme_color) _applyAccentColor(data.theme_color, { persist: true });
|
|
1513
1512
|
Brand.clearBrandCache();
|
|
1514
1513
|
Brand.applyHeaderLogo();
|
|
1515
1514
|
// Refresh brand status flags (user_licensed may have flipped from false
|
|
@@ -1685,7 +1684,7 @@ const Settings = (() => {
|
|
|
1685
1684
|
// The state object per kind:
|
|
1686
1685
|
// { source, configured, model, base_url, api_key_masked, provider, available }
|
|
1687
1686
|
|
|
1688
|
-
const MEDIA_KINDS = ["image", "video", "audio", "ocr"];
|
|
1687
|
+
const MEDIA_KINDS = ["image", "video", "audio", "stt", "video_understanding", "ocr"];
|
|
1689
1688
|
let _mediaState = null;
|
|
1690
1689
|
let _mediaDefaults = null;
|
|
1691
1690
|
const _mediaCustomDraft = {};
|
|
@@ -2175,6 +2174,8 @@ const Settings = (() => {
|
|
|
2175
2174
|
Brand.applyBrandName("OpenClacky");
|
|
2176
2175
|
Brand.clearBrandCache();
|
|
2177
2176
|
Brand.applyHeaderLogo();
|
|
2177
|
+
const userAccent = (() => { try { return localStorage.getItem("clacky-accent-color"); } catch (_) { return null; } })();
|
|
2178
|
+
_applyAccentColor(userAccent || ACCENT_DEFAULT, { persist: false });
|
|
2178
2179
|
// Reset Skills panel state (hide Brand Skills tab, switch to My Skills)
|
|
2179
2180
|
if (typeof Skills !== "undefined" && Skills.resetAfterUnbind) {
|
|
2180
2181
|
Skills.resetAfterUnbind();
|
|
@@ -2205,6 +2206,7 @@ const Settings = (() => {
|
|
|
2205
2206
|
_initLangBtns();
|
|
2206
2207
|
_initFontBtns();
|
|
2207
2208
|
_initCurrencyBtns();
|
|
2209
|
+
_initAccentColorBtns();
|
|
2208
2210
|
|
|
2209
2211
|
// Re-render model cards when language changes (dynamic HTML, not data-i18n)
|
|
2210
2212
|
document.addEventListener("langchange", () => {
|
|
@@ -2366,6 +2368,34 @@ const Settings = (() => {
|
|
|
2366
2368
|
});
|
|
2367
2369
|
}
|
|
2368
2370
|
|
|
2371
|
+
// ── Accent Color ──────────────────────────────────────────────────────
|
|
2372
|
+
const ACCENT_STORAGE_KEY = "clacky-accent-color";
|
|
2373
|
+
const ACCENT_DEFAULT = "#4f46e5";
|
|
2374
|
+
|
|
2375
|
+
function _applyAccentColor(color, { persist = true } = {}) {
|
|
2376
|
+
const root = document.documentElement;
|
|
2377
|
+
root.style.setProperty("--color-accent-primary", color);
|
|
2378
|
+
root.style.setProperty("--color-accent-hover", color);
|
|
2379
|
+
root.style.setProperty("--color-button-primary", color);
|
|
2380
|
+
root.style.setProperty("--color-button-primary-hover", `color-mix(in srgb, ${color} 82%, #000)`);
|
|
2381
|
+
if (persist) {
|
|
2382
|
+
try { localStorage.setItem(ACCENT_STORAGE_KEY, color); } catch (_) {}
|
|
2383
|
+
}
|
|
2384
|
+
document.querySelectorAll("#accent-color-section .settings-accent-swatch").forEach(btn => {
|
|
2385
|
+
btn.classList.toggle("active", btn.dataset.color === color);
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function _initAccentColorBtns() {
|
|
2390
|
+
let saved = null;
|
|
2391
|
+
try { saved = localStorage.getItem(ACCENT_STORAGE_KEY); } catch (_) {}
|
|
2392
|
+
_applyAccentColor(saved || ACCENT_DEFAULT, { persist: !!saved });
|
|
2393
|
+
|
|
2394
|
+
document.querySelectorAll("#accent-color-section .settings-accent-swatch").forEach(btn => {
|
|
2395
|
+
btn.addEventListener("click", () => _applyAccentColor(btn.dataset.color));
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2369
2399
|
// ── QR Code Lightbox ───────────────────────────────────────────────────
|
|
2370
2400
|
// Sets up click-to-enlarge behaviour for the support QR code.
|
|
2371
2401
|
// Safe to call multiple times — idempotent via a data attribute guard.
|
|
@@ -2406,5 +2436,5 @@ const Settings = (() => {
|
|
|
2406
2436
|
});
|
|
2407
2437
|
}
|
|
2408
2438
|
|
|
2409
|
-
return { open, init, loadBrand: _loadBrand };
|
|
2439
|
+
return { open, init, loadBrand: _loadBrand, applyAccentColor: _applyAccentColor };
|
|
2410
2440
|
})();
|
|
@@ -216,8 +216,8 @@ WS.onEvent(ev => {
|
|
|
216
216
|
// send it now — after restoreFromHash has settled — so appendMsg won't be wiped.
|
|
217
217
|
const pendingMsg = Sessions.takePendingMessage();
|
|
218
218
|
if (pendingMsg && pendingMsg.session_id === ev.session_id) {
|
|
219
|
-
Sessions.appendMsg("user", escapeHtml(pendingMsg.content), { time: new Date() });
|
|
220
|
-
WS.send({ type: "message", session_id: pendingMsg.session_id, content: pendingMsg.content });
|
|
219
|
+
Sessions.appendMsg("user", escapeHtml(pendingMsg.display || pendingMsg.content), { time: new Date() });
|
|
220
|
+
WS.send({ type: "message", session_id: pendingMsg.session_id, content: pendingMsg.content, lang: I18n.lang() });
|
|
221
221
|
}
|
|
222
222
|
break;
|
|
223
223
|
}
|
|
@@ -298,14 +298,21 @@ 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":
|
|
306
310
|
if (ev.session_id !== Sessions.activeId) break;
|
|
307
311
|
Sessions.clearProgress();
|
|
308
312
|
Sessions.appendMsg("assistant", ev.content);
|
|
313
|
+
if (window.Clacky && Clacky.ext) {
|
|
314
|
+
Clacky.ext.emit("session:assistant-message", { sessionId: ev.session_id, content: ev.content });
|
|
315
|
+
}
|
|
309
316
|
break;
|
|
310
317
|
|
|
311
318
|
case "tool_call":
|
|
@@ -438,10 +445,15 @@ function renderErrorEvent(ev) {
|
|
|
438
445
|
const action = ev.top_up_url
|
|
439
446
|
? ` <a href="${escapeHtml(ev.top_up_url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(I18n.t("error.insufficient_credit.action"))} →</a>`
|
|
440
447
|
: "";
|
|
441
|
-
Sessions.appendMsg("error", `<span>${body}${action}</span
|
|
448
|
+
Sessions.appendMsg("error", `<span>${body}${action}</span>${_buildRawDetail(ev.raw_message)}`);
|
|
442
449
|
return;
|
|
443
450
|
}
|
|
444
|
-
Sessions.appendMsg("error", escapeHtml(ev.message));
|
|
451
|
+
Sessions.appendMsg("error", escapeHtml(ev.message) + _buildRawDetail(ev.raw_message));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function _buildRawDetail(raw) {
|
|
455
|
+
if (!raw) return "";
|
|
456
|
+
return `<details class="error-raw-detail"><summary>${escapeHtml(I18n.t("error.show_detail"))}</summary><pre>${escapeHtml(raw)}</pre></details>`;
|
|
445
457
|
}
|
|
446
458
|
|
|
447
459
|
window.renderErrorEvent = renderErrorEvent;
|
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,15 +145,37 @@ 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
|
-
class AgentError < StandardError
|
|
151
|
-
|
|
158
|
+
class AgentError < StandardError
|
|
159
|
+
attr_reader :raw_message
|
|
160
|
+
|
|
161
|
+
def initialize(message, raw_message: nil)
|
|
162
|
+
super(message)
|
|
163
|
+
@raw_message = raw_message
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
class BadRequestError < AgentError
|
|
167
|
+
attr_reader :display_message
|
|
168
|
+
|
|
169
|
+
def initialize(message, display_message: nil, raw_message: nil)
|
|
170
|
+
super(message, raw_message: raw_message)
|
|
171
|
+
@display_message = display_message
|
|
172
|
+
end
|
|
173
|
+
end
|
|
152
174
|
class InsufficientCreditError < AgentError
|
|
153
175
|
attr_reader :error_code, :provider_id
|
|
154
176
|
|
|
155
|
-
def initialize(message, error_code: nil, provider_id: nil)
|
|
156
|
-
super(message)
|
|
177
|
+
def initialize(message, error_code: nil, provider_id: nil, raw_message: nil)
|
|
178
|
+
super(message, raw_message: raw_message)
|
|
157
179
|
@error_code = error_code
|
|
158
180
|
@provider_id = provider_id
|
|
159
181
|
end
|
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.5
|
|
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
|
|
@@ -322,6 +338,7 @@ files:
|
|
|
322
338
|
- lib/clacky/aes_gcm.rb
|
|
323
339
|
- lib/clacky/agent.rb
|
|
324
340
|
- lib/clacky/agent/cost_tracker.rb
|
|
341
|
+
- lib/clacky/agent/fake_tool_call_detector.rb
|
|
325
342
|
- lib/clacky/agent/hook_manager.rb
|
|
326
343
|
- lib/clacky/agent/llm_caller.rb
|
|
327
344
|
- lib/clacky/agent/memory_updater.rb
|
|
@@ -339,6 +356,8 @@ files:
|
|
|
339
356
|
- lib/clacky/agent_config.rb
|
|
340
357
|
- lib/clacky/agent_profile.rb
|
|
341
358
|
- lib/clacky/anthropic_stream_aggregator.rb
|
|
359
|
+
- lib/clacky/api_extension.rb
|
|
360
|
+
- lib/clacky/api_extension_loader.rb
|
|
342
361
|
- lib/clacky/banner.rb
|
|
343
362
|
- lib/clacky/bedrock_stream_aggregator.rb
|
|
344
363
|
- lib/clacky/billing/billing_record.rb
|
|
@@ -357,6 +376,10 @@ files:
|
|
|
357
376
|
- lib/clacky/default_agents/coding/webui/.gitkeep
|
|
358
377
|
- lib/clacky/default_agents/general/profile.yml
|
|
359
378
|
- lib/clacky/default_agents/general/system_prompt.md
|
|
379
|
+
- lib/clacky/default_extensions/meeting/handler.rb
|
|
380
|
+
- lib/clacky/default_extensions/meeting/meeting.js
|
|
381
|
+
- lib/clacky/default_extensions/meeting/meta.yml
|
|
382
|
+
- lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md
|
|
360
383
|
- lib/clacky/default_parsers/doc_parser.rb
|
|
361
384
|
- lib/clacky/default_parsers/docx_parser.rb
|
|
362
385
|
- lib/clacky/default_parsers/pdf_parser.rb
|
|
@@ -381,6 +404,7 @@ files:
|
|
|
381
404
|
- lib/clacky/default_skills/extend-openclacky/SKILL.md
|
|
382
405
|
- lib/clacky/default_skills/mcp-manager/SKILL.md
|
|
383
406
|
- lib/clacky/default_skills/media-gen/SKILL.md
|
|
407
|
+
- lib/clacky/default_skills/media-gen/scripts/video_seq.sh
|
|
384
408
|
- lib/clacky/default_skills/new/SKILL.md
|
|
385
409
|
- lib/clacky/default_skills/new/scripts/create_rails_project.sh
|
|
386
410
|
- lib/clacky/default_skills/onboard/SKILL.md
|
|
@@ -412,6 +436,9 @@ files:
|
|
|
412
436
|
- lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb
|
|
413
437
|
- lib/clacky/idle_compression_timer.rb
|
|
414
438
|
- lib/clacky/json_ui_controller.rb
|
|
439
|
+
- lib/clacky/locales/en.rb
|
|
440
|
+
- lib/clacky/locales/i18n.rb
|
|
441
|
+
- lib/clacky/locales/zh.rb
|
|
415
442
|
- lib/clacky/mcp/client.rb
|
|
416
443
|
- lib/clacky/mcp/http_transport.rb
|
|
417
444
|
- lib/clacky/mcp/registry.rb
|
|
@@ -424,18 +451,34 @@ files:
|
|
|
424
451
|
- lib/clacky/media/gemini.rb
|
|
425
452
|
- lib/clacky/media/generator.rb
|
|
426
453
|
- lib/clacky/media/openai_compat.rb
|
|
427
|
-
- lib/clacky/media/output_dir.rb
|
|
428
454
|
- lib/clacky/message_format/anthropic.rb
|
|
429
455
|
- lib/clacky/message_format/bedrock.rb
|
|
430
456
|
- lib/clacky/message_format/open_ai.rb
|
|
431
457
|
- lib/clacky/message_history.rb
|
|
458
|
+
- lib/clacky/null_ui_controller.rb
|
|
432
459
|
- lib/clacky/openai_stream_aggregator.rb
|
|
433
460
|
- lib/clacky/patch_loader.rb
|
|
434
461
|
- lib/clacky/plain_ui_controller.rb
|
|
435
462
|
- lib/clacky/platform_http_client.rb
|
|
436
463
|
- lib/clacky/providers.rb
|
|
437
464
|
- lib/clacky/proxy_config.rb
|
|
465
|
+
- lib/clacky/rich_ui.rb
|
|
466
|
+
- lib/clacky/rich_ui/components/base_component.rb
|
|
467
|
+
- lib/clacky/rich_ui/components/dialogs/approval_dialog.rb
|
|
468
|
+
- lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb
|
|
469
|
+
- lib/clacky/rich_ui/components/dialogs/form_dialog.rb
|
|
470
|
+
- lib/clacky/rich_ui/components/sidebar.rb
|
|
471
|
+
- lib/clacky/rich_ui/components/sidebar_panels.rb
|
|
472
|
+
- lib/clacky/rich_ui/components/status_view.rb
|
|
473
|
+
- lib/clacky/rich_ui/components/thinking_live_view.rb
|
|
474
|
+
- lib/clacky/rich_ui/entry_tracker.rb
|
|
475
|
+
- lib/clacky/rich_ui/layout_adapter.rb
|
|
476
|
+
- lib/clacky/rich_ui/progress_handle_adapter.rb
|
|
477
|
+
- lib/clacky/rich_ui/rich_ui_controller.rb
|
|
478
|
+
- lib/clacky/rich_ui/shell/rich_agent_shell.rb
|
|
479
|
+
- lib/clacky/rich_ui/view_renderer.rb
|
|
438
480
|
- lib/clacky/rich_ui_controller.rb
|
|
481
|
+
- lib/clacky/server/api_extension_dispatcher.rb
|
|
439
482
|
- lib/clacky/server/backup_manager.rb
|
|
440
483
|
- lib/clacky/server/browser_manager.rb
|
|
441
484
|
- 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
|