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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/docs/rich_ui_guide.md +277 -0
  4. data/docs/rich_ui_refactor_plan.md +396 -0
  5. data/lib/clacky/agent/llm_caller.rb +10 -4
  6. data/lib/clacky/agent/session_serializer.rb +3 -2
  7. data/lib/clacky/agent.rb +3 -2
  8. data/lib/clacky/agent_config.rb +2 -14
  9. data/lib/clacky/api_extension.rb +262 -0
  10. data/lib/clacky/api_extension_loader.rb +156 -0
  11. data/lib/clacky/cli.rb +93 -3
  12. data/lib/clacky/client.rb +38 -13
  13. data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
  14. data/lib/clacky/default_agents/_panels/time_machine/panel.js +1 -1
  15. data/lib/clacky/default_skills/media-gen/SKILL.md +9 -6
  16. data/lib/clacky/idle_compression_timer.rb +3 -1
  17. data/lib/clacky/locales/en.rb +26 -0
  18. data/lib/clacky/locales/i18n.rb +26 -0
  19. data/lib/clacky/locales/zh.rb +26 -0
  20. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  21. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  22. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  23. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  24. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  25. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  26. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  27. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  28. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  29. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  30. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  31. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  32. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  33. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  34. data/lib/clacky/rich_ui.rb +57 -0
  35. data/lib/clacky/rich_ui_controller.rb +3 -1549
  36. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  37. data/lib/clacky/server/http_server.rb +150 -103
  38. data/lib/clacky/server/session_registry.rb +1 -1
  39. data/lib/clacky/shell_hook_loader.rb +1 -1
  40. data/lib/clacky/tools/edit.rb +14 -2
  41. data/lib/clacky/ui2/ui_controller.rb +7 -0
  42. data/lib/clacky/version.rb +1 -1
  43. data/lib/clacky/web/app.css +56 -59
  44. data/lib/clacky/web/app.js +65 -7
  45. data/lib/clacky/web/components/onboard.js +18 -2
  46. data/lib/clacky/web/core/aside.js +8 -3
  47. data/lib/clacky/web/core/ext.js +1 -1
  48. data/lib/clacky/web/features/skills/store.js +30 -2
  49. data/lib/clacky/web/features/skills/view.js +32 -1
  50. data/lib/clacky/web/features/workspace/view.js +1 -1
  51. data/lib/clacky/web/i18n.js +32 -20
  52. data/lib/clacky/web/index.html +9 -17
  53. data/lib/clacky/web/sessions.js +286 -28
  54. data/lib/clacky/web/settings.js +109 -111
  55. data/lib/clacky/web/ws-dispatcher.js +7 -3
  56. data/lib/clacky.rb +17 -2
  57. metadata +38 -2
  58. data/lib/clacky/media/output_dir.rb +0 -43
@@ -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": _openModal(index); break;
145
- case "test": _testModel(index); break;
146
- case "delete": _removeModel(index); break;
147
- case "default": _setAsDefault(index); break;
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
- saveBtn.textContent = I18n.t("settings.models.btn.save");
380
- saveBtn.disabled = false;
381
- _showModalTestResult(false, I18n.t("settings.models.placeholder.apikey"));
382
- return;
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
- const isOpen = modelDropdown.style.display === "block";
467
- document.querySelectorAll(".model-name-dropdown, .base-url-dropdown").forEach(d => {
468
- d.style.display = "none";
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
- const isOpen = baseUrlDropdown.style.display === "block";
484
- document.querySelectorAll(".model-name-dropdown, .base-url-dropdown").forEach(d => {
485
- d.style.display = "none";
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 all modal dropdowns on document click
499
- document.addEventListener("click", () => {
500
- modelDropdown.style.display = "none";
501
- baseUrlDropdown.style.display = "none";
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 only during history replay never from live WS.
302
- // Rendered by Sessions._fetchHistory; nothing to do here.
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; end # 400 errors — our request was malformed, history should be rolled back
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.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