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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -0
- data/lib/clacky/agent/chunk_index.rb +83 -0
- data/lib/clacky/agent/history_navigation.rb +239 -0
- data/lib/clacky/agent/session_serializer.rb +53 -98
- data/lib/clacky/agent.rb +477 -297
- data/lib/clacky/agent_config.rb +5 -3
- data/lib/clacky/billing/billing_store.rb +2 -2
- data/lib/clacky/billing/platform_billing.rb +7 -0
- data/lib/clacky/brand_config.rb +27 -19
- data/lib/clacky/cli.rb +92 -12
- data/lib/clacky/cli_guidance.rb +48 -0
- data/lib/clacky/client.rb +2 -2
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +307 -98
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
- data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
- data/lib/clacky/default_extensions/preview/ext.yml +20 -0
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
- data/lib/clacky/extension/api_extension.rb +18 -6
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/json_ui_controller.rb +4 -2
- data/lib/clacky/media/openai_compat.rb +70 -30
- data/lib/clacky/message_format/bedrock.rb +6 -1
- data/lib/clacky/message_format/open_ai.rb +22 -5
- data/lib/clacky/message_format/open_ai_responses.rb +7 -3
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/providers.rb +44 -13
- data/lib/clacky/rich_ui/components/composer_guidance.rb +49 -0
- data/lib/clacky/rich_ui/rich_ui_controller.rb +24 -4
- data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +13 -0
- data/lib/clacky/search_config.rb +3 -3
- data/lib/clacky/server/channel/channel_manager.rb +25 -7
- data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
- data/lib/clacky/server/dir_picker.rb +67 -4
- data/lib/clacky/server/git_panel.rb +10 -2
- data/lib/clacky/server/http_server.rb +391 -75
- data/lib/clacky/server/preview.rb +351 -0
- data/lib/clacky/server/session_registry.rb +4 -0
- data/lib/clacky/server/web_ui_controller.rb +10 -5
- data/lib/clacky/session_manager.rb +52 -1
- data/lib/clacky/skill.rb +48 -0
- data/lib/clacky/tools/browser.rb +176 -19
- data/lib/clacky/tools/web_search.rb +91 -8
- data/lib/clacky/ui2/components/command_suggestions.rb +3 -1
- data/lib/clacky/ui2/components/input_area.rb +37 -4
- data/lib/clacky/ui2/components/modal_component.rb +35 -6
- data/lib/clacky/ui2/screen_buffer.rb +1 -0
- data/lib/clacky/ui2/ui_controller.rb +72 -12
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +19 -7
- data/lib/clacky/utils/mac_app_detector.rb +186 -0
- data/lib/clacky/utils/model_pricing.rb +118 -68
- data/lib/clacky/utils/windows_app_detector.rb +334 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +998 -168
- data/lib/clacky/web/app.js +19 -0
- data/lib/clacky/web/components/chat-navigator.js +489 -202
- data/lib/clacky/web/components/code-editor.js +191 -5
- data/lib/clacky/web/components/composer.js +3 -1
- data/lib/clacky/web/components/datepicker.js +19 -19
- data/lib/clacky/web/components/mentions.js +11 -14
- data/lib/clacky/web/components/model-picker.js +66 -23
- data/lib/clacky/web/components/quote-select.js +341 -0
- data/lib/clacky/web/core/aside.js +157 -8
- data/lib/clacky/web/core/ext.js +16 -0
- data/lib/clacky/web/features/billing/view.js +75 -21
- data/lib/clacky/web/features/extensions/store.js +4 -1
- data/lib/clacky/web/features/model-tester/store.js +37 -4
- data/lib/clacky/web/features/skills/store.js +18 -1
- data/lib/clacky/web/features/skills/view.js +128 -7
- data/lib/clacky/web/features/workspace/store.js +80 -7
- data/lib/clacky/web/features/workspace/view.js +474 -43
- data/lib/clacky/web/i18n.js +158 -23
- data/lib/clacky/web/index.html +57 -29
- data/lib/clacky/web/sessions.js +517 -127
- data/lib/clacky/web/settings.js +155 -6
- data/lib/clacky/web/utils.js +34 -0
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
- data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
- data/lib/clacky/web/vendor/codemirror/package.json +29 -0
- data/lib/clacky/web/ws-dispatcher.js +161 -4
- data/lib/clacky.rb +2 -0
- metadata +13 -1
data/lib/clacky/web/settings.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
828
|
-
`<div class="model-dropdown-
|
|
829
|
-
|
|
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");
|
data/lib/clacky/web/utils.js
CHANGED
|
@@ -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;
|