openclacky 1.5.11 → 1.5.13
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 +82 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/cost_tracker.rb +2 -0
- data/lib/clacky/agent/message_compressor_helper.rb +53 -2
- data/lib/clacky/agent/session_serializer.rb +72 -19
- data/lib/clacky/agent/time_machine.rb +10 -1
- data/lib/clacky/agent.rb +107 -26
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/billing_record.rb +3 -0
- data/lib/clacky/billing/billing_store.rb +31 -5
- data/lib/clacky/billing/platform_billing.rb +231 -0
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cancel_flag.rb +23 -0
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/advisor/advisors/general.md +23 -17
- data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +104 -5
- data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +92 -75
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +31 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +74 -24
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +16 -37
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -67
- data/lib/clacky/default_extensions/meeting/ext.yml +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +1 -1
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +186 -208
- data/lib/clacky/extension/packager.rb +4 -2
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +2 -3
- data/lib/clacky/extension/verifier.rb +14 -1
- data/lib/clacky/fanout.rb +49 -2
- data/lib/clacky/json_ui_controller.rb +3 -2
- data/lib/clacky/message_format/open_ai.rb +5 -1
- data/lib/clacky/message_format/open_ai_responses.rb +409 -0
- data/lib/clacky/message_history.rb +43 -1
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/plain_ui_controller.rb +15 -5
- data/lib/clacky/providers.rb +24 -7
- data/lib/clacky/rich_ui/rich_ui_controller.rb +2 -2
- data/lib/clacky/server/channel/channel_manager.rb +93 -18
- data/lib/clacky/server/channel/channel_ui_controller.rb +16 -3
- data/lib/clacky/server/dir_picker.rb +154 -0
- data/lib/clacky/server/git_panel.rb +61 -9
- data/lib/clacky/server/http_server.rb +366 -42
- data/lib/clacky/server/web_ui_controller.rb +7 -5
- data/lib/clacky/session_manager.rb +74 -17
- data/lib/clacky/tools/ask_user.rb +3 -2
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/ui2/markdown_renderer.rb +15 -12
- data/lib/clacky/ui2/strings_cjk_patch.rb +132 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +2 -2
- data/lib/clacky/ui_interface.rb +2 -2
- data/lib/clacky/utils/model_pricing.rb +92 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +915 -142
- data/lib/clacky/web/components/code-editor.js +33 -1
- data/lib/clacky/web/components/composer.js +315 -0
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +655 -0
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/aside.js +3 -3
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/store.js +2 -6
- data/lib/clacky/web/features/billing/view.js +32 -16
- data/lib/clacky/web/features/extensions/view.js +22 -10
- data/lib/clacky/web/features/new-session/store.js +15 -1
- data/lib/clacky/web/features/new-session/view.js +80 -34
- data/lib/clacky/web/features/trash/view.js +31 -19
- data/lib/clacky/web/features/workspace/store.js +1 -0
- data/lib/clacky/web/features/workspace/view.js +766 -80
- data/lib/clacky/web/i18n.js +107 -17
- data/lib/clacky/web/index.html +125 -45
- data/lib/clacky/web/sessions.js +704 -635
- data/lib/clacky/web/settings.js +164 -190
- data/lib/clacky/web/skills.js +11 -17
- data/lib/clacky/web/ws-dispatcher.js +11 -33
- data/lib/clacky.rb +4 -0
- metadata +12 -1
data/lib/clacky/web/settings.js
CHANGED
|
@@ -12,6 +12,9 @@ const Settings = (() => {
|
|
|
12
12
|
// backend's runtime inference decide.
|
|
13
13
|
let _modalSelectedProviderId = null;
|
|
14
14
|
let _modalProviderIsCustom = false;
|
|
15
|
+
let _apiFormatSelect = null;
|
|
16
|
+
let _providerSelect = null;
|
|
17
|
+
let _providerFilterSelect = null;
|
|
15
18
|
// Live filter text for the model search box; empty = show all.
|
|
16
19
|
let _modelSearchQuery = "";
|
|
17
20
|
// Provider id selected in the model provider filter; empty = show all.
|
|
@@ -83,8 +86,10 @@ const Settings = (() => {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
function _renderProviderFilter() {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
89
|
+
const wrap = document.getElementById("model-provider-filter");
|
|
90
|
+
if (!wrap) return;
|
|
91
|
+
const dropdown = wrap.querySelector(".custom-select-dropdown");
|
|
92
|
+
if (!dropdown) return;
|
|
88
93
|
|
|
89
94
|
// Count models per provider bucket so the dropdown can sort by usage
|
|
90
95
|
// (most-used providers first), with display name as a stable tiebreaker.
|
|
@@ -99,12 +104,13 @@ const Settings = (() => {
|
|
|
99
104
|
}
|
|
100
105
|
});
|
|
101
106
|
|
|
102
|
-
const prev =
|
|
103
|
-
|
|
104
|
-
const all = document.createElement("
|
|
105
|
-
all.
|
|
107
|
+
const prev = _providerFilterSelect ? _providerFilterSelect.value : "";
|
|
108
|
+
dropdown.innerHTML = "";
|
|
109
|
+
const all = document.createElement("div");
|
|
110
|
+
all.className = "custom-select-option";
|
|
111
|
+
all.dataset.value = "";
|
|
106
112
|
all.textContent = I18n.t("settings.models.filter.all");
|
|
107
|
-
|
|
113
|
+
dropdown.appendChild(all);
|
|
108
114
|
|
|
109
115
|
[...names.entries()]
|
|
110
116
|
.sort((a, b) => {
|
|
@@ -112,17 +118,18 @@ const Settings = (() => {
|
|
|
112
118
|
return diff !== 0 ? diff : a[1].localeCompare(b[1]);
|
|
113
119
|
})
|
|
114
120
|
.forEach(([key, name]) => {
|
|
115
|
-
const opt = document.createElement("
|
|
116
|
-
opt.
|
|
121
|
+
const opt = document.createElement("div");
|
|
122
|
+
opt.className = "custom-select-option";
|
|
123
|
+
opt.dataset.value = key;
|
|
117
124
|
opt.textContent = name;
|
|
118
|
-
|
|
125
|
+
dropdown.appendChild(opt);
|
|
119
126
|
});
|
|
120
127
|
|
|
121
128
|
// Keep the current selection when it still exists, otherwise reset to All.
|
|
122
129
|
if (prev && names.has(prev)) {
|
|
123
|
-
|
|
130
|
+
if (_providerFilterSelect) _providerFilterSelect.setValue(prev);
|
|
124
131
|
} else {
|
|
125
|
-
|
|
132
|
+
if (_providerFilterSelect) _providerFilterSelect.setValue("");
|
|
126
133
|
_modelProviderFilter = "";
|
|
127
134
|
}
|
|
128
135
|
}
|
|
@@ -165,9 +172,14 @@ const Settings = (() => {
|
|
|
165
172
|
function _getProvider(model) {
|
|
166
173
|
// Prefer the saved provider_id (survives custom base_url edits) over
|
|
167
174
|
// a reverse base_url lookup so the card always shows what the user picked.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
// "custom" is the persisted marker for user-added custom entries (also
|
|
176
|
+
// implied by an api_format override): neither should reverse-match a
|
|
177
|
+
// preset — the badge and website link must reflect the user's choice.
|
|
178
|
+
if (model.provider_id) {
|
|
179
|
+
return _providers.find(pr => pr.id === model.provider_id) || null;
|
|
180
|
+
}
|
|
181
|
+
if (model.api_format) return null;
|
|
182
|
+
return _findProviderByBaseUrl(model.base_url);
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
// Stable key used to bucket models by provider for the filter dropdown.
|
|
@@ -192,14 +204,28 @@ const Settings = (() => {
|
|
|
192
204
|
}) || null;
|
|
193
205
|
}
|
|
194
206
|
|
|
207
|
+
// Shared resolution for the provider dropdown (edit + duplicate).
|
|
208
|
+
// Priority: the persisted "custom" marker; saved preset provider_id;
|
|
209
|
+
// explicit api_format override (also marks custom — the override dropdown
|
|
210
|
+
// only exists in custom mode, and a base_url reverse lookup must not
|
|
211
|
+
// re-attach a preset here, because preset mode sends api_format:null on
|
|
212
|
+
// save and would silently drop the override); legacy base_url lookup;
|
|
213
|
+
// anthropic_format self-hosted proxy hint.
|
|
214
|
+
function _resolveModalProviderId(model) {
|
|
215
|
+
if (model.provider_id === "custom") return null;
|
|
216
|
+
if (model.provider_id) return model.provider_id;
|
|
217
|
+
if (model.api_format) return null;
|
|
218
|
+
const matched = _findProviderByBaseUrl(model.base_url);
|
|
219
|
+
if (matched) return matched.id;
|
|
220
|
+
return model.anthropic_format ? "anthropic" : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
195
223
|
function _renderCard(container, model, index) {
|
|
196
224
|
const isDefault = model.type === "default";
|
|
197
225
|
const isLite = model.type === "lite";
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
const provider = (model
|
|
201
|
-
? _providers.find(p => p.id === model.provider_id) || null
|
|
202
|
-
: null) || _findProviderByBaseUrl(model.base_url);
|
|
226
|
+
// Same resolution rule as _getProvider: saved provider_id wins, an
|
|
227
|
+
// api_format override marks custom, base_url lookup is legacy fallback.
|
|
228
|
+
const provider = _getProvider(model);
|
|
203
229
|
const providerName = _providerDisplayName(provider);
|
|
204
230
|
const remark = (model.remark || "").trim();
|
|
205
231
|
const websiteUrl = provider && provider.website_url;
|
|
@@ -249,6 +275,7 @@ const Settings = (() => {
|
|
|
249
275
|
</div>
|
|
250
276
|
<div class="model-card-grid-footer">
|
|
251
277
|
${websiteUrl ? `<a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer" style="margin-left:auto">
|
|
278
|
+
<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="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg>
|
|
252
279
|
${I18n.t("settings.models.link.topUp")}
|
|
253
280
|
<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>
|
|
254
281
|
</a>` : ""}
|
|
@@ -294,7 +321,7 @@ const Settings = (() => {
|
|
|
294
321
|
document.getElementById("model-modal-model").value = model.model || "";
|
|
295
322
|
document.getElementById("model-modal-baseurl").value = model.base_url || "";
|
|
296
323
|
document.getElementById("model-modal-apikey").value = model.api_key_masked || "";
|
|
297
|
-
|
|
324
|
+
if (_apiFormatSelect) _apiFormatSelect.setValue(model.api_format || "");
|
|
298
325
|
document.getElementById("model-modal-default-field").style.display = "";
|
|
299
326
|
// Lock the checkbox when this is the only configured model: the system
|
|
300
327
|
// must always have one default (backend re-promotes on save), so
|
|
@@ -305,14 +332,10 @@ const Settings = (() => {
|
|
|
305
332
|
setDefaultCb.checked = isOnlyModel ? true : (model.type === "default");
|
|
306
333
|
setDefaultCb.disabled = isOnlyModel;
|
|
307
334
|
|
|
308
|
-
// Set provider dropdown value
|
|
309
|
-
//
|
|
310
|
-
// survives
|
|
311
|
-
|
|
312
|
-
const matched = _findProviderByBaseUrl(model.base_url);
|
|
313
|
-
const savedProviderId = model.provider_id || null;
|
|
314
|
-
const resolvedProviderId = savedProviderId ||
|
|
315
|
-
(matched ? matched.id : (model.anthropic_format ? "anthropic" : null));
|
|
335
|
+
// Set provider dropdown value (shared resolution: see
|
|
336
|
+
// _resolveModalProviderId). An api_format override marks custom so the
|
|
337
|
+
// override survives reopen+save instead of being silently dropped.
|
|
338
|
+
const resolvedProviderId = _resolveModalProviderId(model);
|
|
316
339
|
_modalSelectedProviderId = resolvedProviderId;
|
|
317
340
|
_modalProviderIsCustom = !resolvedProviderId;
|
|
318
341
|
_syncApiFormatVisibility();
|
|
@@ -337,7 +360,7 @@ const Settings = (() => {
|
|
|
337
360
|
document.getElementById("model-modal-apikey").value = "";
|
|
338
361
|
document.getElementById("model-modal-remark").value = "";
|
|
339
362
|
document.getElementById("model-modal-no-vision").checked = false;
|
|
340
|
-
|
|
363
|
+
if (_apiFormatSelect) _apiFormatSelect.setValue("");
|
|
341
364
|
document.getElementById("model-modal-default-field").style.display = "";
|
|
342
365
|
// Default to checked for new models - most users want their first/new
|
|
343
366
|
// model to take over as the default.
|
|
@@ -392,12 +415,16 @@ const Settings = (() => {
|
|
|
392
415
|
document.getElementById("model-modal-baseurl").value = source.base_url || "";
|
|
393
416
|
document.getElementById("model-modal-apikey").value = source.api_key_masked || "";
|
|
394
417
|
document.getElementById("model-modal-remark").value = source.remark || "";
|
|
418
|
+
if (_apiFormatSelect) _apiFormatSelect.setValue(source.api_format || "");
|
|
395
419
|
|
|
396
|
-
const
|
|
397
|
-
_modalSelectedProviderId =
|
|
398
|
-
_modalProviderIsCustom = !
|
|
420
|
+
const resolvedProviderId = _resolveModalProviderId(source);
|
|
421
|
+
_modalSelectedProviderId = resolvedProviderId;
|
|
422
|
+
_modalProviderIsCustom = !resolvedProviderId;
|
|
399
423
|
_syncApiFormatVisibility();
|
|
400
424
|
const providerValue = document.getElementById("model-modal-provider-value");
|
|
425
|
+
const matched = resolvedProviderId
|
|
426
|
+
? _providers.find(p => p.id === resolvedProviderId) || null
|
|
427
|
+
: null;
|
|
401
428
|
if (matched) {
|
|
402
429
|
providerValue.textContent = matched.name;
|
|
403
430
|
providerValue.classList.remove("placeholder");
|
|
@@ -428,68 +455,18 @@ const Settings = (() => {
|
|
|
428
455
|
|
|
429
456
|
function _populateModalProviderDropdown() {
|
|
430
457
|
const dropdown = document.getElementById("model-modal-provider-dropdown");
|
|
458
|
+
// The provider list scrolls on its own while the "Custom" entry sits in
|
|
459
|
+
// a fixed footer area outside the scroll container, so it never scrolls.
|
|
431
460
|
dropdown.innerHTML = `
|
|
432
|
-
<div class="custom-select-
|
|
433
|
-
|
|
434
|
-
|
|
461
|
+
<div class="custom-select-list">
|
|
462
|
+
<div class="custom-select-option" data-value="">${I18n.t("settings.models.placeholder.provider")}</div>
|
|
463
|
+
${_providers.map(p => { const dn = _providerDisplayName(p); return `<div class="custom-select-option" data-value="${p.id}" data-label="${_esc(dn)}">${_esc(dn)}${p.id === "openclacky" ? ` <span class="provider-badge-recommended">${I18n.t("provider.recommended")}</span>` : ""}</div>`; }).join("")}
|
|
464
|
+
</div>
|
|
465
|
+
<div class="custom-select-option custom-select-option-custom" data-value="custom" data-label="${_esc(I18n.t("settings.models.custom"))}">
|
|
466
|
+
<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 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
|
467
|
+
<span>${I18n.t("settings.models.custom")}</span>
|
|
468
|
+
</div>
|
|
435
469
|
`;
|
|
436
|
-
|
|
437
|
-
// Bind click events for options
|
|
438
|
-
dropdown.querySelectorAll(".custom-select-option").forEach(option => {
|
|
439
|
-
option.addEventListener("click", (e) => {
|
|
440
|
-
e.stopPropagation();
|
|
441
|
-
const value = option.dataset.value;
|
|
442
|
-
const text = option.dataset.label || option.textContent.trim();
|
|
443
|
-
|
|
444
|
-
// Track the picked provider so test/save can flag anthropic_format=true
|
|
445
|
-
// when the user explicitly picks Anthropic. Empty / "custom" → null.
|
|
446
|
-
_modalSelectedProviderId = (value && value !== "custom") ? value : null;
|
|
447
|
-
_modalProviderIsCustom = value === "custom";
|
|
448
|
-
_syncApiFormatVisibility();
|
|
449
|
-
|
|
450
|
-
const providerValue = document.getElementById("model-modal-provider-value");
|
|
451
|
-
providerValue.textContent = text;
|
|
452
|
-
providerValue.classList.toggle("placeholder", !value);
|
|
453
|
-
_syncRemarkField();
|
|
454
|
-
|
|
455
|
-
_closeProviderDropdown();
|
|
456
|
-
|
|
457
|
-
// Show/hide promo hint
|
|
458
|
-
const promoHint = document.getElementById("model-modal-promo-hint");
|
|
459
|
-
if (value === "openclacky" || !value) {
|
|
460
|
-
_showPromoHint(promoHint);
|
|
461
|
-
} else {
|
|
462
|
-
promoHint.classList.remove("visible");
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Auto-fill if provider selected
|
|
466
|
-
if (value && value !== "custom") {
|
|
467
|
-
const preset = _providers.find(p => p.id === value);
|
|
468
|
-
if (preset) {
|
|
469
|
-
document.getElementById("model-modal-model").value = preset.default_model || "";
|
|
470
|
-
document.getElementById("model-modal-baseurl").value = preset.base_url || "";
|
|
471
|
-
|
|
472
|
-
const apikeyLink = document.getElementById("model-modal-apikey-link");
|
|
473
|
-
if (preset.website_url) {
|
|
474
|
-
apikeyLink.href = preset.website_url;
|
|
475
|
-
apikeyLink.style.display = "";
|
|
476
|
-
} else {
|
|
477
|
-
apikeyLink.style.display = "none";
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Update model dropdown with provider's models
|
|
481
|
-
setTimeout(() => _updateModalModelDropdown(), 0);
|
|
482
|
-
}
|
|
483
|
-
} else {
|
|
484
|
-
// Selecting "custom" (or the placeholder) abandons the preset:
|
|
485
|
-
// clear leftover model/base_url so stale values never survive.
|
|
486
|
-
document.getElementById("model-modal-model").value = "";
|
|
487
|
-
document.getElementById("model-modal-baseurl").value = "";
|
|
488
|
-
document.getElementById("model-modal-apikey-link").style.display = "none";
|
|
489
|
-
setTimeout(() => _updateModalModelDropdown(), 0);
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
470
|
}
|
|
494
471
|
|
|
495
472
|
async function _testModel(index) {
|
|
@@ -527,7 +504,7 @@ const Settings = (() => {
|
|
|
527
504
|
// Preset providers own their protocol routing; drop any override so the
|
|
528
505
|
// backend falls back to preset + per-model resolution (overrides are a
|
|
529
506
|
// custom-provider feature).
|
|
530
|
-
const api_format = _modalProviderIsCustom ? (
|
|
507
|
+
const api_format = _modalProviderIsCustom ? ((_apiFormatSelect && _apiFormatSelect.value) || null) : null;
|
|
531
508
|
|
|
532
509
|
saveBtn.disabled = true;
|
|
533
510
|
|
|
@@ -570,7 +547,10 @@ const Settings = (() => {
|
|
|
570
547
|
|
|
571
548
|
const hasId = !!existingId;
|
|
572
549
|
|
|
573
|
-
|
|
550
|
+
// Persist the custom choice as provider_id:"custom" so reopening the
|
|
551
|
+
// modal (and the card badge) stays custom even when base_url matches a
|
|
552
|
+
// preset endpoint; null would fall into the legacy base_url reverse lookup.
|
|
553
|
+
const payload = { model, base_url, anthropic_format, api_format, provider_id: _modalProviderIsCustom ? "custom" : (_modalSelectedProviderId || null) };
|
|
574
554
|
payload.remark = document.getElementById("model-modal-remark").value.trim();
|
|
575
555
|
// Vision override only applies to custom (non-preset) providers — preset
|
|
576
556
|
// capability tables live server-side. Checked → vision:false; unchecked →
|
|
@@ -644,25 +624,11 @@ const Settings = (() => {
|
|
|
644
624
|
}
|
|
645
625
|
|
|
646
626
|
function _openProviderDropdown() {
|
|
647
|
-
|
|
648
|
-
const trigger = document.getElementById("model-modal-provider-trigger");
|
|
649
|
-
const dropdown = document.getElementById("model-modal-provider-dropdown");
|
|
650
|
-
_positionDropdownFixed(dropdown, wrapper);
|
|
651
|
-
dropdown.classList.add("open");
|
|
652
|
-
dropdown.style.display = "block";
|
|
653
|
-
trigger.classList.add("open");
|
|
654
|
-
document.body.appendChild(dropdown);
|
|
627
|
+
if (_providerSelect) _providerSelect.open();
|
|
655
628
|
}
|
|
656
629
|
|
|
657
630
|
function _closeProviderDropdown() {
|
|
658
|
-
|
|
659
|
-
const trigger = document.getElementById("model-modal-provider-trigger");
|
|
660
|
-
const dropdown = document.getElementById("model-modal-provider-dropdown");
|
|
661
|
-
dropdown.classList.remove("open");
|
|
662
|
-
dropdown.style.display = "none";
|
|
663
|
-
trigger.classList.remove("open");
|
|
664
|
-
_resetDropdownPosition(dropdown);
|
|
665
|
-
wrapper.appendChild(dropdown);
|
|
631
|
+
if (_providerSelect) _providerSelect.close();
|
|
666
632
|
}
|
|
667
633
|
|
|
668
634
|
function _initModal() {
|
|
@@ -686,21 +652,63 @@ const Settings = (() => {
|
|
|
686
652
|
});
|
|
687
653
|
|
|
688
654
|
// Provider dropdown toggle (fixed-positioned like the model/base-url dropdowns)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
655
|
+
_providerSelect = CustomSelect.init({
|
|
656
|
+
trigger: document.getElementById("model-modal-provider-trigger"),
|
|
657
|
+
dropdown: document.getElementById("model-modal-provider-dropdown"),
|
|
658
|
+
anchor: document.getElementById("model-modal-provider-wrapper"),
|
|
659
|
+
portal: true,
|
|
660
|
+
onSelect: function (value) {
|
|
661
|
+
// Track the picked provider so test/save can flag anthropic_format=true
|
|
662
|
+
// when the user explicitly picks Anthropic. Empty / "custom" → null.
|
|
663
|
+
_modalSelectedProviderId = (value && value !== "custom") ? value : null;
|
|
664
|
+
_modalProviderIsCustom = value === "custom";
|
|
665
|
+
_syncApiFormatVisibility();
|
|
666
|
+
_syncRemarkField();
|
|
667
|
+
|
|
668
|
+
// Show/hide promo hint
|
|
669
|
+
const promoHint = document.getElementById("model-modal-promo-hint");
|
|
670
|
+
if (value === "openclacky" || !value) {
|
|
671
|
+
_showPromoHint(promoHint);
|
|
672
|
+
} else {
|
|
673
|
+
promoHint.classList.remove("visible");
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Auto-fill if provider selected
|
|
677
|
+
if (value && value !== "custom") {
|
|
678
|
+
const preset = _providers.find(p => p.id === value);
|
|
679
|
+
if (preset) {
|
|
680
|
+
document.getElementById("model-modal-model").value = preset.default_model || "";
|
|
681
|
+
document.getElementById("model-modal-baseurl").value = preset.base_url || "";
|
|
682
|
+
|
|
683
|
+
const apikeyLink = document.getElementById("model-modal-apikey-link");
|
|
684
|
+
if (preset.website_url) {
|
|
685
|
+
apikeyLink.href = preset.website_url;
|
|
686
|
+
apikeyLink.style.display = "";
|
|
687
|
+
} else {
|
|
688
|
+
apikeyLink.style.display = "none";
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Update model dropdown with provider's models
|
|
692
|
+
setTimeout(() => _updateModalModelDropdown(), 0);
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
// Selecting "custom" (or the placeholder) abandons the preset:
|
|
696
|
+
// clear leftover model/base_url so stale values never survive.
|
|
697
|
+
document.getElementById("model-modal-model").value = "";
|
|
698
|
+
document.getElementById("model-modal-baseurl").value = "";
|
|
699
|
+
document.getElementById("model-modal-apikey-link").style.display = "none";
|
|
700
|
+
setTimeout(() => _updateModalModelDropdown(), 0);
|
|
701
|
+
}
|
|
697
702
|
}
|
|
698
703
|
});
|
|
699
704
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
705
|
+
const apiFormatSelect = CustomSelect.init({
|
|
706
|
+
trigger: document.getElementById("model-modal-api-format-trigger"),
|
|
707
|
+
dropdown: document.getElementById("model-modal-api-format-dropdown"),
|
|
708
|
+
anchor: document.getElementById("model-modal-api-format-wrapper"),
|
|
709
|
+
portal: true
|
|
703
710
|
});
|
|
711
|
+
_apiFormatSelect = apiFormatSelect;
|
|
704
712
|
|
|
705
713
|
// Toggle API key visibility
|
|
706
714
|
document.getElementById("model-modal-toggle-key").addEventListener("click", () => {
|
|
@@ -875,12 +883,9 @@ const Settings = (() => {
|
|
|
875
883
|
}
|
|
876
884
|
|
|
877
885
|
function _bindCardEvents(card, index) {
|
|
878
|
-
// Custom dropdown interactions
|
|
879
886
|
const customSelectWrapper = card.querySelector(".custom-select-wrapper");
|
|
880
887
|
const trigger = customSelectWrapper.querySelector(".custom-select-trigger");
|
|
881
888
|
const dropdown = customSelectWrapper.querySelector(".custom-select-dropdown");
|
|
882
|
-
const valueSpan = trigger.querySelector(".custom-select-value");
|
|
883
|
-
const options = dropdown.querySelectorAll(".custom-select-option");
|
|
884
889
|
|
|
885
890
|
// Initialize promo hint: only show for new cards (no existing model config)
|
|
886
891
|
const quickSetupField = card.querySelector(".quick-setup-field");
|
|
@@ -892,44 +897,10 @@ const Settings = (() => {
|
|
|
892
897
|
_showPromoHint(initialPromoHint);
|
|
893
898
|
}
|
|
894
899
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
// Close all other dropdowns
|
|
900
|
-
document.querySelectorAll(".custom-select-dropdown.open").forEach(d => {
|
|
901
|
-
d.classList.remove("open");
|
|
902
|
-
d.previousElementSibling.classList.remove("open");
|
|
903
|
-
});
|
|
904
|
-
if (!isOpen) {
|
|
905
|
-
dropdown.classList.add("open");
|
|
906
|
-
trigger.classList.add("open");
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
// Select option
|
|
911
|
-
options.forEach(option => {
|
|
912
|
-
option.addEventListener("click", (e) => {
|
|
913
|
-
e.stopPropagation();
|
|
914
|
-
const value = option.dataset.value;
|
|
915
|
-
const text = option.dataset.label || option.textContent;
|
|
916
|
-
|
|
917
|
-
// Update UI
|
|
918
|
-
valueSpan.textContent = text;
|
|
919
|
-
if (value) {
|
|
920
|
-
valueSpan.classList.remove("placeholder");
|
|
921
|
-
} else {
|
|
922
|
-
valueSpan.classList.add("placeholder");
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Update selected state
|
|
926
|
-
options.forEach(opt => opt.classList.remove("selected"));
|
|
927
|
-
option.classList.add("selected");
|
|
928
|
-
|
|
929
|
-
// Close dropdown
|
|
930
|
-
dropdown.classList.remove("open");
|
|
931
|
-
trigger.classList.remove("open");
|
|
932
|
-
|
|
900
|
+
CustomSelect.init({
|
|
901
|
+
trigger: trigger,
|
|
902
|
+
dropdown: dropdown,
|
|
903
|
+
onSelect: function (value) {
|
|
933
904
|
// Auto-fill model & base_url if a provider preset was selected
|
|
934
905
|
const getApiKeyLink = card.querySelector(`.get-apikey-link[data-index="${index}"]`);
|
|
935
906
|
const promoHint = card.querySelector(`.provider-promo-hint[data-index="${index}"]`);
|
|
@@ -961,13 +932,7 @@ const Settings = (() => {
|
|
|
961
932
|
// Show promo hint when no provider selected (default state)
|
|
962
933
|
if (promoHint) _showPromoHint(promoHint);
|
|
963
934
|
}
|
|
964
|
-
}
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
// Close dropdown when clicking outside
|
|
968
|
-
document.addEventListener("click", () => {
|
|
969
|
-
dropdown.classList.remove("open");
|
|
970
|
-
trigger.classList.remove("open");
|
|
935
|
+
}
|
|
971
936
|
});
|
|
972
937
|
|
|
973
938
|
// Toggle API key visibility
|
|
@@ -1563,30 +1528,37 @@ const Settings = (() => {
|
|
|
1563
1528
|
// ── Web Search ────────────────────────────────────────────────────────────────
|
|
1564
1529
|
|
|
1565
1530
|
async function _initSearchSettings() {
|
|
1566
|
-
const
|
|
1531
|
+
const trigger = document.getElementById("settings-search-provider-trigger");
|
|
1532
|
+
const dropdown = document.getElementById("settings-search-provider-dropdown");
|
|
1567
1533
|
const keyWrap = document.getElementById("search-key-wrap");
|
|
1568
1534
|
const keyInput = document.getElementById("settings-search-key");
|
|
1569
1535
|
const saveBtn = document.getElementById("btn-save-search");
|
|
1570
1536
|
const testBtn = document.getElementById("btn-test-search");
|
|
1571
1537
|
const status = document.getElementById("settings-search-status");
|
|
1572
|
-
if (!
|
|
1538
|
+
if (!dropdown || !saveBtn) return;
|
|
1539
|
+
|
|
1540
|
+
let select = dropdown.__customSelect;
|
|
1541
|
+
if (!select && trigger) {
|
|
1542
|
+
select = CustomSelect.init({
|
|
1543
|
+
trigger: trigger,
|
|
1544
|
+
dropdown: dropdown,
|
|
1545
|
+
onSelect: function () { _syncKeyVisibility(); }
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1573
1548
|
|
|
1574
1549
|
function _syncKeyVisibility() {
|
|
1575
|
-
keyWrap.style.display = select.value ? "" : "none";
|
|
1550
|
+
keyWrap.style.display = (select && select.value) ? "" : "none";
|
|
1576
1551
|
}
|
|
1577
1552
|
|
|
1578
1553
|
try {
|
|
1579
1554
|
const res = await fetch("/api/config/search");
|
|
1580
1555
|
const data = await res.json();
|
|
1581
1556
|
if (data.ok) {
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
select.appendChild(opt);
|
|
1588
|
-
});
|
|
1589
|
-
select.value = data.provider || "";
|
|
1557
|
+
const rows = (data.available || []).map(name =>
|
|
1558
|
+
`<div class="custom-select-option" data-value="${_esc(name)}">${_esc(name)}</div>`).join("");
|
|
1559
|
+
dropdown.innerHTML =
|
|
1560
|
+
`<div class="custom-select-option" data-value="">${I18n.t("settings.search.builtin")}</div>` + rows;
|
|
1561
|
+
if (select) select.setValue(data.provider || "");
|
|
1590
1562
|
keyInput.value = data.key_masked || "";
|
|
1591
1563
|
}
|
|
1592
1564
|
} catch (_) { /* non-critical */ }
|
|
@@ -1597,11 +1569,6 @@ const Settings = (() => {
|
|
|
1597
1569
|
status.className = `model-test-result ${ok ? "success" : "error"}`;
|
|
1598
1570
|
}
|
|
1599
1571
|
|
|
1600
|
-
if (!select.dataset.bound) {
|
|
1601
|
-
select.dataset.bound = "1";
|
|
1602
|
-
select.addEventListener("change", _syncKeyVisibility);
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
1572
|
if (!saveBtn.dataset.bound) {
|
|
1606
1573
|
saveBtn.dataset.bound = "1";
|
|
1607
1574
|
saveBtn.addEventListener("click", async () => {
|
|
@@ -2579,10 +2546,17 @@ const Settings = (() => {
|
|
|
2579
2546
|
_modelSearchQuery = e.target.value;
|
|
2580
2547
|
_renderCards();
|
|
2581
2548
|
});
|
|
2582
|
-
document.getElementById("model-provider-filter")
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2549
|
+
const providerFilterWrap = document.getElementById("model-provider-filter");
|
|
2550
|
+
if (providerFilterWrap && window.CustomSelect) {
|
|
2551
|
+
_providerFilterSelect = window.CustomSelect.init({
|
|
2552
|
+
trigger: providerFilterWrap.querySelector(".custom-select-trigger"),
|
|
2553
|
+
dropdown: providerFilterWrap.querySelector(".custom-select-dropdown"),
|
|
2554
|
+
onSelect: (value) => {
|
|
2555
|
+
_modelProviderFilter = value;
|
|
2556
|
+
_renderCards();
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2586
2560
|
document.getElementById("btn-rerun-onboard").addEventListener("click", _rerunOnboard);
|
|
2587
2561
|
document.getElementById("btn-browser-setup").addEventListener("click", _setupBrowser);
|
|
2588
2562
|
|
data/lib/clacky/web/skills.js
CHANGED
|
@@ -377,10 +377,8 @@ const SkillAC = (() => {
|
|
|
377
377
|
const skill = _items[idx];
|
|
378
378
|
if (!skill) return;
|
|
379
379
|
_mruTouch(skill.name);
|
|
380
|
-
const input
|
|
381
|
-
input
|
|
382
|
-
input.style.height = "auto";
|
|
383
|
-
input.style.height = Math.min(input.scrollHeight, 200) + "px";
|
|
380
|
+
const input = _inputEl();
|
|
381
|
+
Composer.setText(input, "/" + skill.name + " ");
|
|
384
382
|
_hide();
|
|
385
383
|
input.focus();
|
|
386
384
|
}
|
|
@@ -425,10 +423,9 @@ const SkillAC = (() => {
|
|
|
425
423
|
$(cfg.slashBtn).addEventListener("click", () => {
|
|
426
424
|
_cfg = cfg;
|
|
427
425
|
const input = $(cfg.input);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
input
|
|
431
|
-
input.style.height = Math.min(input.scrollHeight, 200) + "px";
|
|
426
|
+
const value = Composer.text(input);
|
|
427
|
+
if (value === "" || value === "/") {
|
|
428
|
+
Composer.setText(input, "/");
|
|
432
429
|
}
|
|
433
430
|
_toggle(); // Toggle dropdown instead of always opening
|
|
434
431
|
if (_visible) {
|
|
@@ -453,22 +450,19 @@ const SkillAC = (() => {
|
|
|
453
450
|
}
|
|
454
451
|
});
|
|
455
452
|
|
|
456
|
-
// Composer input:
|
|
453
|
+
// Composer input: drive skill autocomplete; normalize full-width slash.
|
|
457
454
|
$(cfg.input).addEventListener("input", () => {
|
|
458
455
|
_cfg = cfg;
|
|
459
456
|
const el = $(cfg.input);
|
|
460
|
-
el.style.height = "auto";
|
|
461
|
-
el.style.height = Math.min(el.scrollHeight, 200) + "px";
|
|
462
457
|
|
|
463
458
|
// Replace full-width slash / or Chinese dunhao 、 with ASCII / in-place
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
el
|
|
467
|
-
el.setSelectionRange(pos, pos);
|
|
459
|
+
const value = Composer.text(el);
|
|
460
|
+
if (/^[/、]/.test(value)) {
|
|
461
|
+
Composer.setText(el, value.replace(/^[/、]/, "/"));
|
|
468
462
|
}
|
|
469
463
|
|
|
470
464
|
// Trigger skill autocomplete
|
|
471
|
-
_update(el
|
|
465
|
+
_update(Composer.text(el));
|
|
472
466
|
});
|
|
473
467
|
|
|
474
468
|
const chk = $(cfg.systemChk);
|
|
@@ -479,7 +473,7 @@ const SkillAC = (() => {
|
|
|
479
473
|
_showSystemSkills = chk.checked;
|
|
480
474
|
localStorage.setItem(SKILL_AC_SHOW_SYSTEM_KEY, _showSystemSkills ? "true" : "false");
|
|
481
475
|
if (_visible) {
|
|
482
|
-
const query = _getSlashQuery($(cfg.input)
|
|
476
|
+
const query = _getSlashQuery(Composer.text($(cfg.input)));
|
|
483
477
|
if (query !== null) await _render(query);
|
|
484
478
|
}
|
|
485
479
|
});
|