openclacky 1.3.1 → 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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -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 +65 -11
  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/brand_config.rb +1 -1
  11. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  12. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  13. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  14. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  15. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  16. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  17. data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
  18. data/lib/clacky/media/openai_compat.rb +64 -1
  19. data/lib/clacky/media/output_dir.rb +43 -0
  20. data/lib/clacky/message_history.rb +9 -0
  21. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  22. data/lib/clacky/server/git_panel.rb +115 -0
  23. data/lib/clacky/server/http_server.rb +521 -13
  24. data/lib/clacky/server/server_master.rb +6 -4
  25. data/lib/clacky/utils/environment_detector.rb +16 -0
  26. data/lib/clacky/version.rb +1 -1
  27. data/lib/clacky/web/app.css +512 -60
  28. data/lib/clacky/web/app.js +30 -7
  29. data/lib/clacky/web/components/code-editor.js +197 -0
  30. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  31. data/lib/clacky/web/core/aside.js +112 -0
  32. data/lib/clacky/web/core/ext.js +387 -0
  33. data/lib/clacky/web/features/backup/store.js +92 -0
  34. data/lib/clacky/web/features/backup/view.js +94 -0
  35. data/lib/clacky/web/features/billing/store.js +163 -0
  36. data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
  37. data/lib/clacky/web/features/brand/store.js +110 -0
  38. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  39. data/lib/clacky/web/features/channels/store.js +103 -0
  40. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  41. data/lib/clacky/web/features/creator/store.js +81 -0
  42. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  43. data/lib/clacky/web/features/mcp/store.js +158 -0
  44. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  45. data/lib/clacky/web/features/model-tester/store.js +77 -0
  46. data/lib/clacky/web/features/model-tester/view.js +7 -0
  47. data/lib/clacky/web/features/profile/store.js +170 -0
  48. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  49. data/lib/clacky/web/features/share/store.js +145 -0
  50. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  51. data/lib/clacky/web/features/skills/store.js +303 -0
  52. data/lib/clacky/web/features/skills/view.js +550 -0
  53. data/lib/clacky/web/features/tasks/store.js +135 -0
  54. data/lib/clacky/web/features/tasks/view.js +241 -0
  55. data/lib/clacky/web/features/trash/store.js +242 -0
  56. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  57. data/lib/clacky/web/features/version/store.js +165 -0
  58. data/lib/clacky/web/features/version/view.js +323 -0
  59. data/lib/clacky/web/features/workspace/store.js +99 -0
  60. data/lib/clacky/web/features/workspace/view.js +305 -0
  61. data/lib/clacky/web/i18n.js +60 -6
  62. data/lib/clacky/web/index.html +117 -57
  63. data/lib/clacky/web/sessions.js +221 -25
  64. data/lib/clacky/web/settings.js +121 -25
  65. data/lib/clacky/web/skills.js +3 -821
  66. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  67. data/lib/clacky.rb +1 -0
  68. metadata +45 -20
  69. data/lib/clacky/web/backup.js +0 -119
  70. data/lib/clacky/web/model-tester.js +0 -66
  71. data/lib/clacky/web/tasks.js +0 -365
  72. data/lib/clacky/web/version.js +0 -449
  73. data/lib/clacky/web/workspace.js +0 -212
  74. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  75. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  76. /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
  77. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  78. /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>` : ""}
@@ -104,10 +109,6 @@ const Settings = (() => {
104
109
  </div>
105
110
  </div>
106
111
  <div class="model-card-grid-actions">
107
- ${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default">
108
- <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>
109
- <span>${I18n.t("settings.models.btn.setDefault")}</span>
110
- </button>` : ""}
111
112
  <div class="model-card-grid-toolbar">
112
113
  <button class="btn-card-grid-action" data-index="${index}" data-action="test">
113
114
  <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="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
@@ -123,12 +124,12 @@ const Settings = (() => {
123
124
  </button>` : ""}
124
125
  </div>
125
126
  </div>
126
- ${websiteUrl ? `<div class="model-card-grid-footer">
127
- <a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer">
127
+ <div class="model-card-grid-footer">
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
- </a>
131
- </div>` : ""}
131
+ </a>` : ""}
132
+ </div>
132
133
  `;
133
134
 
134
135
  container.appendChild(card);
@@ -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
  }