openclacky 1.3.2 → 1.3.3

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/Dockerfile +3 -0
  4. data/README.md +1 -1
  5. data/README_JA.md +237 -0
  6. data/lib/clacky/agent/session_serializer.rb +49 -5
  7. data/lib/clacky/agent/time_machine.rb +247 -26
  8. data/lib/clacky/agent.rb +12 -1
  9. data/lib/clacky/agent_config.rb +14 -2
  10. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  11. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  12. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  13. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  14. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  15. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  16. data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
  17. data/lib/clacky/media/openai_compat.rb +64 -1
  18. data/lib/clacky/media/output_dir.rb +43 -0
  19. data/lib/clacky/message_history.rb +9 -0
  20. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  21. data/lib/clacky/server/git_panel.rb +115 -0
  22. data/lib/clacky/server/http_server.rb +497 -12
  23. data/lib/clacky/server/server_master.rb +6 -4
  24. data/lib/clacky/version.rb +1 -1
  25. data/lib/clacky/web/app.css +473 -60
  26. data/lib/clacky/web/app.js +30 -7
  27. data/lib/clacky/web/components/code-editor.js +197 -0
  28. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  29. data/lib/clacky/web/core/aside.js +112 -0
  30. data/lib/clacky/web/core/ext.js +387 -0
  31. data/lib/clacky/web/features/backup/store.js +92 -0
  32. data/lib/clacky/web/features/backup/view.js +94 -0
  33. data/lib/clacky/web/features/billing/store.js +163 -0
  34. data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
  35. data/lib/clacky/web/features/brand/store.js +110 -0
  36. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  37. data/lib/clacky/web/features/channels/store.js +103 -0
  38. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  39. data/lib/clacky/web/features/creator/store.js +81 -0
  40. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  41. data/lib/clacky/web/features/mcp/store.js +158 -0
  42. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  43. data/lib/clacky/web/features/model-tester/store.js +77 -0
  44. data/lib/clacky/web/features/model-tester/view.js +7 -0
  45. data/lib/clacky/web/features/profile/store.js +170 -0
  46. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  47. data/lib/clacky/web/features/share/store.js +145 -0
  48. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  49. data/lib/clacky/web/features/skills/store.js +303 -0
  50. data/lib/clacky/web/features/skills/view.js +550 -0
  51. data/lib/clacky/web/features/tasks/store.js +135 -0
  52. data/lib/clacky/web/features/tasks/view.js +241 -0
  53. data/lib/clacky/web/features/trash/store.js +242 -0
  54. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  55. data/lib/clacky/web/features/version/store.js +165 -0
  56. data/lib/clacky/web/features/version/view.js +323 -0
  57. data/lib/clacky/web/features/workspace/store.js +99 -0
  58. data/lib/clacky/web/features/workspace/view.js +305 -0
  59. data/lib/clacky/web/i18n.js +56 -6
  60. data/lib/clacky/web/index.html +117 -58
  61. data/lib/clacky/web/sessions.js +221 -25
  62. data/lib/clacky/web/settings.js +118 -22
  63. data/lib/clacky/web/skills.js +3 -863
  64. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  65. data/lib/clacky.rb +1 -0
  66. metadata +45 -20
  67. data/lib/clacky/web/backup.js +0 -119
  68. data/lib/clacky/web/model-tester.js +0 -66
  69. data/lib/clacky/web/tasks.js +0 -373
  70. data/lib/clacky/web/version.js +0 -449
  71. data/lib/clacky/web/workspace.js +0 -316
  72. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  73. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  74. /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
  75. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  76. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -17,6 +17,7 @@ const Settings = (() => {
17
17
  function open() {
18
18
  _load();
19
19
  _loadMedia();
20
+ _initMediaOutputDir();
20
21
  _loadBrand();
21
22
  _loadBrowserStatus();
22
23
  _initNetworkSettings();
@@ -96,6 +97,10 @@ const Settings = (() => {
96
97
  <span class="model-card-grid-name">${_esc(displayName)}</span>
97
98
  ${isDefault ? `<span class="badge badge-default">${I18n.t("settings.models.badge.default")}</span>` : ""}
98
99
  ${isLite ? `<span class="badge badge-lite">${I18n.t("settings.models.badge.lite")}</span>` : ""}
100
+ ${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default" style="margin-left:auto">
101
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
102
+ <span>${I18n.t("settings.models.btn.setDefault")}</span>
103
+ </button>` : ""}
99
104
  </div>
100
105
  <div class="model-card-grid-provider">${_esc(providerName)}</div>
101
106
  ${model.api_key_masked ? `<div class="model-card-grid-model">${_esc(model.api_key_masked)}</div>` : ""}
@@ -120,11 +125,7 @@ const Settings = (() => {
120
125
  </div>
121
126
  </div>
122
127
  <div class="model-card-grid-footer">
123
- ${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default">
124
- <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="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
125
- <span>${I18n.t("settings.models.btn.setDefault")}</span>
126
- </button>` : `<span></span>`}
127
- ${websiteUrl ? `<a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer">
128
+ ${websiteUrl ? `<a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer" style="margin-left:auto">
128
129
  ${I18n.t("settings.models.link.topUp")}
129
130
  <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>
130
131
  </a>` : ""}
@@ -169,7 +170,14 @@ const Settings = (() => {
169
170
  document.getElementById("model-modal-baseurl").value = model.base_url || "";
170
171
  document.getElementById("model-modal-apikey").value = model.api_key_masked || "";
171
172
  document.getElementById("model-modal-default-field").style.display = "";
172
- document.getElementById("model-modal-set-default").checked = (model.type === "default");
173
+ // Lock the checkbox when this is the only configured model: the system
174
+ // must always have one default (backend re-promotes on save), so
175
+ // unchecking would be a silent no-op. Force-checked + disabled makes
176
+ // the constraint visible without any extra copy.
177
+ const setDefaultCb = document.getElementById("model-modal-set-default");
178
+ const isOnlyModel = _models.length === 1;
179
+ setDefaultCb.checked = isOnlyModel ? true : (model.type === "default");
180
+ setDefaultCb.disabled = isOnlyModel;
173
181
 
174
182
  // Set provider dropdown value
175
183
  const matched = _findProviderByBaseUrl(model.base_url);
@@ -190,6 +198,9 @@ const Settings = (() => {
190
198
  // Default to checked for new models — most users want their first/new
191
199
  // model to take over as the default.
192
200
  document.getElementById("model-modal-set-default").checked = true;
201
+ // Reset disabled flag in case the previous open was edit-mode on the
202
+ // sole-model lock path.
203
+ document.getElementById("model-modal-set-default").disabled = false;
193
204
 
194
205
  // Reset provider dropdown
195
206
  _modalSelectedProviderId = null;
@@ -1261,6 +1272,89 @@ const Settings = (() => {
1261
1272
  }
1262
1273
  }
1263
1274
 
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
+
1264
1358
  // ── Brand & License ───────────────────────────────────────────────────────────
1265
1359
 
1266
1360
  // Whether the server was started with --brand-test (relaxed key validation).
@@ -1463,22 +1557,24 @@ const Settings = (() => {
1463
1557
  // ── Init ──────────────────────────────────────────────────────────────────────
1464
1558
 
1465
1559
  function _initTabs() {
1466
- const tabs = document.querySelectorAll("#settings-tabs .settings-tab");
1467
- const contents = document.querySelectorAll("#settings-body .settings-tab-content");
1468
-
1469
- tabs.forEach(tab => {
1470
- tab.addEventListener("click", () => {
1471
- const targetTab = tab.dataset.tab;
1472
-
1473
- // Update tab buttons
1474
- tabs.forEach(t => t.classList.toggle("active", t.dataset.tab === targetTab));
1475
-
1476
- // Update tab content panels
1477
- contents.forEach(c => {
1478
- const isActive = c.dataset.tabContent === targetTab;
1479
- c.classList.toggle("active", isActive);
1480
- c.style.display = isActive ? "" : "none";
1481
- });
1560
+ const bar = document.getElementById("settings-tabs");
1561
+ if (!bar) return;
1562
+
1563
+ // Delegated so extension tabs (mounted into the settings.tabs slot after
1564
+ // this runs) switch correctly without re-binding.
1565
+ bar.addEventListener("click", (e) => {
1566
+ const tab = e.target.closest(".settings-tab");
1567
+ if (!tab || !bar.contains(tab)) return;
1568
+ const targetTab = tab.dataset.tab;
1569
+ if (!targetTab) return;
1570
+
1571
+ document.querySelectorAll("#settings-tabs .settings-tab").forEach(t =>
1572
+ t.classList.toggle("active", t.dataset.tab === targetTab));
1573
+
1574
+ document.querySelectorAll("#settings-body .settings-tab-content").forEach(c => {
1575
+ const isActive = c.dataset.tabContent === targetTab;
1576
+ c.classList.toggle("active", isActive);
1577
+ c.style.display = isActive ? "" : "none";
1482
1578
  });
1483
1579
  });
1484
1580
  }