openclacky 1.5.13 → 1.5.15

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/lib/clacky/agent/chunk_index.rb +83 -0
  4. data/lib/clacky/agent/history_navigation.rb +239 -0
  5. data/lib/clacky/agent/session_serializer.rb +53 -98
  6. data/lib/clacky/agent.rb +477 -297
  7. data/lib/clacky/agent_config.rb +5 -3
  8. data/lib/clacky/billing/billing_store.rb +2 -2
  9. data/lib/clacky/billing/platform_billing.rb +7 -0
  10. data/lib/clacky/brand_config.rb +27 -19
  11. data/lib/clacky/cli.rb +92 -12
  12. data/lib/clacky/cli_guidance.rb +48 -0
  13. data/lib/clacky/client.rb +2 -2
  14. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
  15. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -1
  16. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +307 -98
  17. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
  18. data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
  19. data/lib/clacky/default_extensions/preview/ext.yml +20 -0
  20. data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
  21. data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
  22. data/lib/clacky/extension/api_extension.rb +18 -6
  23. data/lib/clacky/extension/verifier.rb +1 -1
  24. data/lib/clacky/json_ui_controller.rb +4 -2
  25. data/lib/clacky/media/openai_compat.rb +70 -30
  26. data/lib/clacky/message_format/bedrock.rb +6 -1
  27. data/lib/clacky/message_format/open_ai.rb +22 -5
  28. data/lib/clacky/message_format/open_ai_responses.rb +7 -3
  29. data/lib/clacky/plain_ui_controller.rb +1 -1
  30. data/lib/clacky/prompts/base.md +1 -1
  31. data/lib/clacky/providers.rb +44 -13
  32. data/lib/clacky/rich_ui/components/composer_guidance.rb +49 -0
  33. data/lib/clacky/rich_ui/rich_ui_controller.rb +24 -4
  34. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +13 -0
  35. data/lib/clacky/search_config.rb +3 -3
  36. data/lib/clacky/server/channel/channel_manager.rb +25 -7
  37. data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
  38. data/lib/clacky/server/dir_picker.rb +67 -4
  39. data/lib/clacky/server/git_panel.rb +10 -2
  40. data/lib/clacky/server/http_server.rb +391 -75
  41. data/lib/clacky/server/preview.rb +351 -0
  42. data/lib/clacky/server/session_registry.rb +4 -0
  43. data/lib/clacky/server/web_ui_controller.rb +10 -5
  44. data/lib/clacky/session_manager.rb +52 -1
  45. data/lib/clacky/skill.rb +48 -0
  46. data/lib/clacky/tools/browser.rb +176 -19
  47. data/lib/clacky/tools/web_search.rb +91 -8
  48. data/lib/clacky/ui2/components/command_suggestions.rb +3 -1
  49. data/lib/clacky/ui2/components/input_area.rb +37 -4
  50. data/lib/clacky/ui2/components/modal_component.rb +35 -6
  51. data/lib/clacky/ui2/screen_buffer.rb +1 -0
  52. data/lib/clacky/ui2/ui_controller.rb +72 -12
  53. data/lib/clacky/ui_interface.rb +1 -1
  54. data/lib/clacky/utils/file_processor.rb +19 -7
  55. data/lib/clacky/utils/mac_app_detector.rb +186 -0
  56. data/lib/clacky/utils/model_pricing.rb +118 -68
  57. data/lib/clacky/utils/windows_app_detector.rb +334 -0
  58. data/lib/clacky/version.rb +1 -1
  59. data/lib/clacky/web/app.css +998 -168
  60. data/lib/clacky/web/app.js +19 -0
  61. data/lib/clacky/web/components/chat-navigator.js +489 -202
  62. data/lib/clacky/web/components/code-editor.js +191 -5
  63. data/lib/clacky/web/components/composer.js +3 -1
  64. data/lib/clacky/web/components/datepicker.js +19 -19
  65. data/lib/clacky/web/components/mentions.js +11 -14
  66. data/lib/clacky/web/components/model-picker.js +66 -23
  67. data/lib/clacky/web/components/quote-select.js +341 -0
  68. data/lib/clacky/web/core/aside.js +157 -8
  69. data/lib/clacky/web/core/ext.js +16 -0
  70. data/lib/clacky/web/features/billing/view.js +75 -21
  71. data/lib/clacky/web/features/extensions/store.js +4 -1
  72. data/lib/clacky/web/features/model-tester/store.js +37 -4
  73. data/lib/clacky/web/features/skills/store.js +18 -1
  74. data/lib/clacky/web/features/skills/view.js +128 -7
  75. data/lib/clacky/web/features/workspace/store.js +80 -7
  76. data/lib/clacky/web/features/workspace/view.js +474 -43
  77. data/lib/clacky/web/i18n.js +158 -23
  78. data/lib/clacky/web/index.html +57 -29
  79. data/lib/clacky/web/sessions.js +517 -127
  80. data/lib/clacky/web/settings.js +155 -6
  81. data/lib/clacky/web/utils.js +34 -0
  82. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
  83. data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
  84. data/lib/clacky/web/vendor/codemirror/package.json +29 -0
  85. data/lib/clacky/web/ws-dispatcher.js +161 -4
  86. data/lib/clacky.rb +2 -0
  87. metadata +13 -1
@@ -15,6 +15,14 @@ const Settings = (() => {
15
15
  let _apiFormatSelect = null;
16
16
  let _providerSelect = null;
17
17
  let _providerFilterSelect = null;
18
+ // Model list pulled from an endpoint for the modal's custom-provider flow:
19
+ // `{ key, models }` where key is base_url+api_key, so editing either field
20
+ // silently discards a list fetched for the previous pair. Filled by the
21
+ // automatic fetch triggered on API-key blur / Base-URL blur / dropdown open.
22
+ let _modalRemoteModels = null;
23
+ // Guards against overlapping fetches (blur → open-dropdown can fire within
24
+ // the same second).
25
+ let _remoteLoadInFlight = false;
18
26
  // Live filter text for the model search box; empty = show all.
19
27
  let _modelSearchQuery = "";
20
28
  // Provider id selected in the model provider filter; empty = show all.
@@ -310,6 +318,8 @@ const Settings = (() => {
310
318
 
311
319
  indexInput.value = index;
312
320
  document.getElementById("model-modal-source-id").value = "";
321
+ // Drop any model list fetched for a previously opened model/endpoint.
322
+ _modalRemoteModels = null;
313
323
 
314
324
  // Populate provider dropdown
315
325
  _populateModalProviderDropdown();
@@ -735,6 +745,11 @@ const Settings = (() => {
735
745
  _positionDropdownFixed(modelDropdown, modelCombobox);
736
746
  modelDropdown.style.display = "block";
737
747
  document.body.appendChild(modelDropdown);
748
+ // Opening the dropdown is an explicit "show me what this endpoint
749
+ // offers" intent — fetch when there is no fresh list yet. Masked keys
750
+ // are allowed on this trigger only: the server resolves them from the
751
+ // stored config, so editing a saved custom model can still list it.
752
+ _loadRemoteModels({ allowMasked: true });
738
753
  }
739
754
 
740
755
  function _closeModelDropdown() {
@@ -782,9 +797,19 @@ const Settings = (() => {
782
797
  }
783
798
  });
784
799
 
785
- // Update model dropdown when base_url changes
800
+ // Update model dropdown when base_url changes, and refresh the fetched
801
+ // list for the new endpoint (skipped for presets/masked keys — see
802
+ // _shouldFetchRemoteModels).
786
803
  baseUrlInput.addEventListener("blur", () => {
787
804
  _updateModalModelDropdown();
805
+ _loadRemoteModels();
806
+ });
807
+
808
+ // Finishing the API key is the moment a custom endpoint becomes listable:
809
+ // fetch its models in the background so the dropdown is already populated
810
+ // when the user opens it.
811
+ document.getElementById("model-modal-apikey").addEventListener("blur", () => {
812
+ _loadRemoteModels();
788
813
  });
789
814
 
790
815
  // Close dropdowns on outside click
@@ -817,16 +842,29 @@ const Settings = (() => {
817
842
  const modelDropdown = document.getElementById("model-modal-model-dropdown");
818
843
  const modelInput = document.getElementById("model-modal-model");
819
844
  const provider = _getModalCurrentProvider();
820
- const models = provider?.models || [];
845
+ const picked = _modalDropdownModels(provider);
821
846
 
822
- if (models.length === 0) {
847
+ if (picked.models.length === 0) {
823
848
  modelDropdown.innerHTML = `<div class="model-dropdown-empty">${I18n.t("settings.models.noModels") || "No preset models available"}</div>`;
824
849
  return;
825
850
  }
826
851
 
827
- modelDropdown.innerHTML = models.map(m =>
828
- `<div class="model-dropdown-option" data-value="${_esc(m)}">${_esc(m)}</div>`
829
- ).join("");
852
+ const status = picked.remote
853
+ ? `<div class="model-dropdown-status">${I18n.t("settings.models.fetchedFrom", { endpoint: _esc(picked.endpoint), count: picked.models.length })}</div>`
854
+ : "";
855
+
856
+ modelDropdown.innerHTML = status;
857
+ picked.models.forEach(m => {
858
+ const opt = document.createElement("div");
859
+ opt.className = "model-dropdown-option";
860
+ opt.dataset.value = m;
861
+ opt.appendChild(ModelPicker.vendorBadge(m));
862
+ const label = document.createElement("span");
863
+ label.className = "model-dropdown-option-label";
864
+ label.textContent = m;
865
+ opt.appendChild(label);
866
+ modelDropdown.appendChild(opt);
867
+ });
830
868
 
831
869
  modelDropdown.querySelectorAll(".model-dropdown-option").forEach(opt => {
832
870
  opt.addEventListener("click", (e) => {
@@ -837,6 +875,117 @@ const Settings = (() => {
837
875
  });
838
876
  }
839
877
 
878
+ // Which model ids the dropdown should offer: the list fetched from the
879
+ // endpoint the user typed wins, but only while that exact base_url+api_key
880
+ // pair is still in the form. Otherwise fall back to the matched provider's
881
+ // preset models.
882
+ function _modalDropdownModels(provider) {
883
+ const baseUrl = (document.getElementById("model-modal-baseurl")?.value || "").trim();
884
+ const apiKey = (document.getElementById("model-modal-apikey")?.value || "").trim();
885
+ const fresh = _modalRemoteModels && _modalRemoteModels.key === _remoteModelsKey(baseUrl, apiKey);
886
+ return fresh
887
+ ? { models: _modalRemoteModels.models, remote: true, endpoint: _prettyEndpoint(baseUrl) }
888
+ : { models: provider?.models || [], remote: false, endpoint: "" };
889
+ }
890
+
891
+ // Cache identity for a fetched list — editing Base URL or API Key should not
892
+ // keep offering models that were listed for the previous pair.
893
+ function _remoteModelsKey(baseUrl, apiKey) {
894
+ return baseUrl + "\u0000" + apiKey;
895
+ }
896
+
897
+ // "https://llm.example.com/v1/" → "llm.example.com/v1" for the status row.
898
+ function _prettyEndpoint(rawUrl) {
899
+ const trimmed = String(rawUrl || "").trim();
900
+ try {
901
+ const parsed = new URL(trimmed);
902
+ return (parsed.host + parsed.pathname).replace(/\/+$/, "");
903
+ } catch (e) {
904
+ return trimmed.replace(/^https?:\/\//, "").replace(/\/+$/, "");
905
+ }
906
+ }
907
+
908
+ function _modalApiFormat() {
909
+ return _modalProviderIsCustom ? ((_apiFormatSelect && _apiFormatSelect.value) || null) : null;
910
+ }
911
+
912
+ // Decide whether a trigger (API-key blur, Base-URL blur, dropdown open)
913
+ // should fetch the endpoint's model list. Shared by all triggers so they
914
+ // cannot double-fire or race each other.
915
+ //
916
+ // `allowMasked` is true only for the dropdown-open trigger: there the user
917
+ // explicitly asked for a list, and the server resolves a masked key back to
918
+ // the stored secret via the model id/index. Blur-triggered fetches skip
919
+ // masked keys (upstream guidance) so merely opening a model for editing
920
+ // never fires a request at the provider.
921
+ function _shouldFetchRemoteModels({ allowMasked }) {
922
+ if (_remoteLoadInFlight) return false;
923
+ const baseUrl = (document.getElementById("model-modal-baseurl")?.value || "").trim();
924
+ const apiKey = (document.getElementById("model-modal-apikey")?.value || "").trim();
925
+ if (!baseUrl || !apiKey) return false;
926
+ if (!allowMasked && apiKey.includes("****")) return false;
927
+ // Preset providers keep their static model list (upstream guidance).
928
+ if (_getModalCurrentProvider()) return false;
929
+ // Same pair already fetched — reuse the cache.
930
+ if (_modalRemoteModels && _modalRemoteModels.key === _remoteModelsKey(baseUrl, apiKey)) {
931
+ return false;
932
+ }
933
+ return true;
934
+ }
935
+
936
+ // Query the typed endpoint for the model ids it advertises and offer them in
937
+ // the Model dropdown. Triggered automatically, never by a dedicated button.
938
+ async function _loadRemoteModels({ allowMasked = false } = {}) {
939
+ if (!_shouldFetchRemoteModels({ allowMasked })) return;
940
+
941
+ const dropdown = document.getElementById("model-modal-model-dropdown");
942
+ const baseUrl = (document.getElementById("model-modal-baseurl")?.value || "").trim();
943
+ const apiKey = (document.getElementById("model-modal-apikey")?.value || "").trim();
944
+ const index = parseInt(document.getElementById("model-modal-index").value, 10);
945
+ const sourceId = document.getElementById("model-modal-source-id").value || null;
946
+ const existing = index >= 0 ? (_models[index] || {}) : {};
947
+
948
+ // Paint progress/errors only while the dropdown is open, checked live at
949
+ // paint time: a blur-triggered fetch runs in the background, and the user
950
+ // may open the dropdown while it is still in flight — the outcome (error,
951
+ // empty, loading) is exactly what they are waiting for then. Failures and
952
+ // empty results are deliberately not cached, so reopening retries.
953
+ function _paint(text) {
954
+ if (dropdown.style.display !== "block") return;
955
+ dropdown.innerHTML = `<div class="model-dropdown-empty">${text}</div>`;
956
+ }
957
+
958
+ _remoteLoadInFlight = true;
959
+ let result;
960
+ try {
961
+ _paint(I18n.t("settings.models.loadingModels") || "Loading models…");
962
+ result = await ModelTester.listModels({
963
+ base_url: baseUrl,
964
+ api_key: apiKey,
965
+ api_format: _modalApiFormat(),
966
+ id: existing.id || sourceId,
967
+ index: index >= 0 ? index : undefined
968
+ });
969
+ } catch (e) {
970
+ result = { ok: false, message: e.message };
971
+ }
972
+ _remoteLoadInFlight = false;
973
+
974
+ if (!result.ok) {
975
+ _paint(I18n.t("settings.models.loadFailed", { msg: _esc(result.message || "") })
976
+ || "Couldn't load models");
977
+ return;
978
+ }
979
+
980
+ if (result.models.length === 0) {
981
+ _paint(I18n.t("settings.models.loadEmpty") || "Endpoint returned no models");
982
+ return;
983
+ }
984
+
985
+ _modalRemoteModels = { key: _remoteModelsKey(baseUrl, apiKey), models: result.models };
986
+ _updateModalModelDropdown();
987
+ }
988
+
840
989
  function _updateModalBaseUrlDropdown() {
841
990
  const baseUrlDropdown = document.getElementById("model-modal-baseurl-dropdown");
842
991
  const baseUrlInput = document.getElementById("model-modal-baseurl");
@@ -56,6 +56,40 @@ const IME = (() => {
56
56
  return { track, bindEnter };
57
57
  })();
58
58
 
59
+ // Compact, non-interactive Markdown for short previews. Only inline formatting
60
+ // is emitted; raw HTML and images cannot introduce active content.
61
+ const MarkdownPreview = (() => {
62
+ function render(text) {
63
+ if (!text) return "";
64
+ if (typeof marked === "undefined") return escapeHtml(text);
65
+
66
+ const renderer = new marked.Renderer();
67
+ renderer.html = renderer.image = renderer.checkbox = () => "";
68
+ renderer.hr = renderer.br = () => " ";
69
+ renderer.link = function({ tokens }) { return this.parser.parseInline(tokens); };
70
+ renderer.paragraph = renderer.heading = function({ tokens }) {
71
+ return this.parser.parseInline(tokens) + " ";
72
+ };
73
+ renderer.blockquote = renderer.listitem = function({ tokens }) {
74
+ return this.parser.parse(tokens).trim() + " ";
75
+ };
76
+ renderer.list = function({ items }) {
77
+ return items.map(item => this.listitem(item)).join("");
78
+ };
79
+ renderer.table = function({ header, rows }) {
80
+ return [header, ...rows].map(row => row.map(cell => this.parser.parseInline(cell.tokens)).join(" ")).join(" ") + " ";
81
+ };
82
+ renderer.code = function({ text }) { return `<code>${escapeHtml(text)}</code> `; };
83
+ try {
84
+ return marked.parse(text, { gfm: true, breaks: false, renderer }).trim();
85
+ } catch (_) {
86
+ return escapeHtml(text);
87
+ }
88
+ }
89
+
90
+ return { render };
91
+ })();
92
+
59
93
  const Tooltip = (() => {
60
94
  const GAP = 8;
61
95
  let el = null;