openclacky 1.0.1 → 1.0.2
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 +18 -0
- data/lib/clacky/agent/llm_caller.rb +185 -0
- data/lib/clacky/agent.rb +53 -2
- data/lib/clacky/default_skills/onboard/SKILL.md +14 -5
- data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +175 -0
- data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +59 -26
- data/lib/clacky/providers.rb +57 -3
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +14 -0
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +10 -0
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +1 -0
- data/lib/clacky/server/channel/channel_manager.rb +12 -4
- data/lib/clacky/server/channel/channel_ui_controller.rb +8 -2
- data/lib/clacky/server/http_server.rb +10 -6
- data/lib/clacky/utils/file_processor.rb +14 -40
- data/lib/clacky/utils/model_pricing.rb +95 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +99 -9
- data/lib/clacky/web/i18n.js +14 -0
- data/lib/clacky/web/index.html +8 -2
- data/lib/clacky/web/onboard.js +77 -1
- data/lib/clacky/web/sessions.js +2 -2
- data/lib/clacky/web/settings.js +127 -6
- data/lib/clacky/web/skills.js +4 -0
- data/lib/clacky.rb +5 -0
- metadata +3 -2
data/lib/clacky/web/app.css
CHANGED
|
@@ -78,6 +78,9 @@
|
|
|
78
78
|
--shadow-md: 0 4px 12px rgba(15, 18, 28, 0.08), 0 2px 4px rgba(15, 18, 28, 0.04);
|
|
79
79
|
--shadow-lg: 0 12px 28px rgba(15, 18, 28, 0.12), 0 4px 8px rgba(15, 18, 28, 0.06);
|
|
80
80
|
|
|
81
|
+
/* Layout — shared dimensions */
|
|
82
|
+
--footer-height: 56px;
|
|
83
|
+
|
|
81
84
|
/* Transitions — unified timing */
|
|
82
85
|
--transition-fast: 120ms ease;
|
|
83
86
|
--transition-base: 160ms ease;
|
|
@@ -2079,7 +2082,6 @@ body {
|
|
|
2079
2082
|
padding: 4px 14px;
|
|
2080
2083
|
background: var(--color-bg-primary);
|
|
2081
2084
|
border-top: 1px solid var(--color-border-secondary);
|
|
2082
|
-
border-bottom: 1px solid var(--color-border-secondary);
|
|
2083
2085
|
font-size: 11px;
|
|
2084
2086
|
color: var(--color-text-secondary);
|
|
2085
2087
|
font-family: var(--font-mono, monospace);
|
|
@@ -2437,6 +2439,9 @@ body {
|
|
|
2437
2439
|
border-top: 1px solid var(--color-border-primary);
|
|
2438
2440
|
background: var(--color-bg-secondary);
|
|
2439
2441
|
flex-shrink: 0;
|
|
2442
|
+
min-height: var(--footer-height);
|
|
2443
|
+
display: flex;
|
|
2444
|
+
flex-direction: column;
|
|
2440
2445
|
}
|
|
2441
2446
|
|
|
2442
2447
|
/* Hide top border when skill autocomplete is visible */
|
|
@@ -2762,9 +2767,11 @@ body {
|
|
|
2762
2767
|
|
|
2763
2768
|
/* ── Input bar ───────────────────────────────────────────────────────────── */
|
|
2764
2769
|
#input-bar {
|
|
2765
|
-
|
|
2770
|
+
margin-top: auto;
|
|
2771
|
+
margin-bottom: auto;
|
|
2772
|
+
padding: 0 16px;
|
|
2766
2773
|
display: flex;
|
|
2767
|
-
gap:
|
|
2774
|
+
gap: 6px;
|
|
2768
2775
|
align-items: center;
|
|
2769
2776
|
background: var(--color-bg-secondary);
|
|
2770
2777
|
}
|
|
@@ -2775,7 +2782,9 @@ body {
|
|
|
2775
2782
|
background: transparent;
|
|
2776
2783
|
color: var(--color-text-secondary);
|
|
2777
2784
|
cursor: pointer;
|
|
2778
|
-
|
|
2785
|
+
width: 28px;
|
|
2786
|
+
height: 28px;
|
|
2787
|
+
padding: 0;
|
|
2779
2788
|
line-height: 1;
|
|
2780
2789
|
flex-shrink: 0;
|
|
2781
2790
|
display: flex;
|
|
@@ -2789,17 +2798,21 @@ body {
|
|
|
2789
2798
|
background: transparent;
|
|
2790
2799
|
color: var(--color-text-secondary);
|
|
2791
2800
|
cursor: pointer;
|
|
2792
|
-
|
|
2801
|
+
width: 28px;
|
|
2802
|
+
height: 28px;
|
|
2803
|
+
padding: 0;
|
|
2793
2804
|
line-height: 1;
|
|
2794
2805
|
flex-shrink: 0;
|
|
2795
2806
|
display: flex;
|
|
2796
2807
|
align-items: center;
|
|
2797
2808
|
justify-content: center;
|
|
2798
2809
|
border-radius: 6px;
|
|
2799
|
-
font-size: 15px;
|
|
2800
2810
|
font-weight: 600;
|
|
2801
2811
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
2802
2812
|
}
|
|
2813
|
+
#btn-slash span {
|
|
2814
|
+
font-size: 18px;
|
|
2815
|
+
}
|
|
2803
2816
|
#btn-slash:hover { color: var(--color-accent-primary); background: var(--color-border-secondary); }
|
|
2804
2817
|
#btn-slash.active { color: var(--color-accent-primary); }
|
|
2805
2818
|
#user-input {
|
|
@@ -2822,24 +2835,32 @@ body {
|
|
|
2822
2835
|
#btn-send, #btn-interrupt {
|
|
2823
2836
|
border: none;
|
|
2824
2837
|
border-radius: 6px;
|
|
2825
|
-
padding:
|
|
2838
|
+
padding: 0 16px;
|
|
2839
|
+
height: 32px;
|
|
2826
2840
|
font-size: 13px;
|
|
2827
2841
|
cursor: pointer;
|
|
2828
2842
|
white-space: nowrap;
|
|
2829
2843
|
flex-shrink: 0;
|
|
2844
|
+
display: flex;
|
|
2845
|
+
align-items: center;
|
|
2846
|
+
justify-content: center;
|
|
2830
2847
|
}
|
|
2831
2848
|
#btn-send { background: var(--color-button-primary); color: #fff; }
|
|
2832
2849
|
#btn-send:hover { background: var(--color-button-primary-hover); }
|
|
2833
2850
|
[data-theme="dark"] #btn-send { background: #2563eb; }
|
|
2834
2851
|
[data-theme="dark"] #btn-send:hover { background: #1d4ed8; }
|
|
2835
2852
|
#btn-send:disabled { background: var(--color-border-primary); color: var(--color-text-secondary); cursor: not-allowed; }
|
|
2836
|
-
#btn-interrupt { background: var(--color-error); color: #fff;
|
|
2853
|
+
#btn-interrupt { background: var(--color-error); color: #fff; }
|
|
2854
|
+
#btn-interrupt::after { content: ''; display: block; width: 10px; height: 10px; background: #fff; border-radius: 2px; }
|
|
2837
2855
|
#btn-interrupt:hover{ background: var(--color-error); opacity: 0.85; }
|
|
2838
2856
|
|
|
2839
2857
|
/* ── Sidebar footer ──────────────────────────────────────────────────────── */
|
|
2840
2858
|
#sidebar-footer {
|
|
2841
2859
|
border-top: 1px solid var(--color-border-primary);
|
|
2842
|
-
|
|
2860
|
+
min-height: var(--footer-height);
|
|
2861
|
+
display: flex;
|
|
2862
|
+
align-items: center;
|
|
2863
|
+
padding: 0 8px;
|
|
2843
2864
|
flex-shrink: 0;
|
|
2844
2865
|
}
|
|
2845
2866
|
.sidebar-nav-btn {
|
|
@@ -3263,6 +3284,74 @@ body {
|
|
|
3263
3284
|
font-style: italic;
|
|
3264
3285
|
}
|
|
3265
3286
|
|
|
3287
|
+
/* Base URL combobox — mirrors model-name-combobox shape; differs only in
|
|
3288
|
+
showing a two-line option (label + URL) so users can distinguish
|
|
3289
|
+
regional/billing-plan variants under one provider. */
|
|
3290
|
+
.base-url-combobox {
|
|
3291
|
+
position: relative;
|
|
3292
|
+
display: flex;
|
|
3293
|
+
gap: 0;
|
|
3294
|
+
}
|
|
3295
|
+
.base-url-combobox .base-url-input {
|
|
3296
|
+
flex: 1;
|
|
3297
|
+
border-top-right-radius: 0;
|
|
3298
|
+
border-bottom-right-radius: 0;
|
|
3299
|
+
border-right: none;
|
|
3300
|
+
}
|
|
3301
|
+
.base-url-combobox .base-url-dropdown-btn {
|
|
3302
|
+
background: var(--color-bg-primary);
|
|
3303
|
+
border: 1px solid var(--color-border-primary);
|
|
3304
|
+
border-top-right-radius: 6px;
|
|
3305
|
+
border-bottom-right-radius: 6px;
|
|
3306
|
+
color: var(--color-text-secondary);
|
|
3307
|
+
padding: 0 8px;
|
|
3308
|
+
cursor: pointer;
|
|
3309
|
+
flex-shrink: 0;
|
|
3310
|
+
display: flex;
|
|
3311
|
+
align-items: center;
|
|
3312
|
+
justify-content: center;
|
|
3313
|
+
transition: all 0.15s ease;
|
|
3314
|
+
}
|
|
3315
|
+
.base-url-combobox .base-url-dropdown-btn:hover {
|
|
3316
|
+
background: var(--color-bg-hover);
|
|
3317
|
+
color: var(--color-text-primary);
|
|
3318
|
+
}
|
|
3319
|
+
.base-url-dropdown {
|
|
3320
|
+
position: absolute;
|
|
3321
|
+
top: calc(100% + 4px);
|
|
3322
|
+
left: 0;
|
|
3323
|
+
right: 0;
|
|
3324
|
+
background: var(--color-bg-secondary);
|
|
3325
|
+
border: 1px solid var(--color-border-primary);
|
|
3326
|
+
border-radius: 8px;
|
|
3327
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
3328
|
+
max-height: 280px;
|
|
3329
|
+
overflow-y: auto;
|
|
3330
|
+
z-index: 1000;
|
|
3331
|
+
}
|
|
3332
|
+
.base-url-dropdown-option {
|
|
3333
|
+
padding: 8px 12px;
|
|
3334
|
+
cursor: pointer;
|
|
3335
|
+
transition: background-color 0.15s ease;
|
|
3336
|
+
}
|
|
3337
|
+
.base-url-dropdown-option:hover {
|
|
3338
|
+
background: var(--color-bg-hover);
|
|
3339
|
+
}
|
|
3340
|
+
.base-url-dropdown-label {
|
|
3341
|
+
font-size: 13px;
|
|
3342
|
+
color: var(--color-text-primary);
|
|
3343
|
+
font-weight: 500;
|
|
3344
|
+
}
|
|
3345
|
+
.base-url-dropdown-option:hover .base-url-dropdown-label {
|
|
3346
|
+
color: var(--color-accent-primary);
|
|
3347
|
+
}
|
|
3348
|
+
.base-url-dropdown-url {
|
|
3349
|
+
font-size: 11px;
|
|
3350
|
+
color: var(--color-text-secondary);
|
|
3351
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Courier New', monospace;
|
|
3352
|
+
margin-top: 2px;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3266
3355
|
/* Model card footer */
|
|
3267
3356
|
.model-card-footer {
|
|
3268
3357
|
display: flex;
|
|
@@ -5748,6 +5837,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
5748
5837
|
border-radius: 6px;
|
|
5749
5838
|
overflow: hidden;
|
|
5750
5839
|
transition: background .15s;
|
|
5840
|
+
width: 100%;
|
|
5751
5841
|
}
|
|
5752
5842
|
.sidebar-nav-row:hover {
|
|
5753
5843
|
background: var(--color-bg-hover);
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -386,6 +386,13 @@ const I18n = (() => {
|
|
|
386
386
|
"settings.models.placeholder.baseurl": "https://api.anthropic.com",
|
|
387
387
|
"settings.models.placeholder.apikey": "sk-…",
|
|
388
388
|
"settings.models.custom": "Custom",
|
|
389
|
+
"settings.models.baseurl.noVariants": "No preset endpoints available",
|
|
390
|
+
"settings.models.baseurl.variant.mainland_cn": "Mainland China",
|
|
391
|
+
"settings.models.baseurl.variant.international": "International",
|
|
392
|
+
"settings.models.baseurl.variant.mainland_cn_payg": "Mainland · Pay-as-you-go",
|
|
393
|
+
"settings.models.baseurl.variant.mainland_cn_coding": "Mainland · Coding Plan",
|
|
394
|
+
"settings.models.baseurl.variant.international_payg": "International · Pay-as-you-go",
|
|
395
|
+
"settings.models.baseurl.variant.international_coding": "International · Coding Plan",
|
|
389
396
|
"settings.models.btn.save": "Save",
|
|
390
397
|
"settings.models.btn.saving": "Saving…",
|
|
391
398
|
"settings.models.btn.saved": "Saved ✓",
|
|
@@ -876,6 +883,13 @@ const I18n = (() => {
|
|
|
876
883
|
"settings.models.placeholder.baseurl": "https://api.anthropic.com",
|
|
877
884
|
"settings.models.placeholder.apikey": "sk-…",
|
|
878
885
|
"settings.models.custom": "自定义",
|
|
886
|
+
"settings.models.baseurl.noVariants": "暂无预设端点",
|
|
887
|
+
"settings.models.baseurl.variant.mainland_cn": "中国大陆",
|
|
888
|
+
"settings.models.baseurl.variant.international": "海外",
|
|
889
|
+
"settings.models.baseurl.variant.mainland_cn_payg": "大陆 · 按量付费",
|
|
890
|
+
"settings.models.baseurl.variant.mainland_cn_coding": "大陆 · Coding Plan",
|
|
891
|
+
"settings.models.baseurl.variant.international_payg": "海外 · 按量付费",
|
|
892
|
+
"settings.models.baseurl.variant.international_coding": "海外 · Coding Plan",
|
|
879
893
|
"settings.models.btn.save": "保存",
|
|
880
894
|
"settings.models.btn.saving": "保存中…",
|
|
881
895
|
"settings.models.btn.saved": "已保存 ✓",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
data-i18n-placeholder="chat.input.placeholder"
|
|
361
361
|
placeholder="Message… (Enter to send, Shift+Enter for newline)"></textarea>
|
|
362
362
|
<button id="btn-send" data-i18n="chat.btn.send">Send</button>
|
|
363
|
-
<button id="btn-interrupt" style="display:none" title="Stop"
|
|
363
|
+
<button id="btn-interrupt" style="display:none" title="Stop"></button>
|
|
364
364
|
</div>
|
|
365
365
|
</div>
|
|
366
366
|
</div>
|
|
@@ -858,7 +858,13 @@
|
|
|
858
858
|
|
|
859
859
|
<div class="setup-field">
|
|
860
860
|
<label class="setup-label" data-i18n="onboard.key.baseurl">Base URL</label>
|
|
861
|
-
<
|
|
861
|
+
<div class="base-url-combobox">
|
|
862
|
+
<input id="setup-base-url" type="text" class="setup-input base-url-input" data-i18n-placeholder="settings.models.placeholder.baseurl" placeholder="https://api.anthropic.com">
|
|
863
|
+
<button type="button" id="setup-base-url-dropdown-btn" class="base-url-dropdown-btn" aria-label="Select preset endpoint">
|
|
864
|
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
865
|
+
</button>
|
|
866
|
+
<div id="setup-base-url-dropdown" class="base-url-dropdown" style="display:none"></div>
|
|
867
|
+
</div>
|
|
862
868
|
</div>
|
|
863
869
|
|
|
864
870
|
<div class="setup-field">
|
data/lib/clacky/web/onboard.js
CHANGED
|
@@ -171,6 +171,60 @@ const Onboard = (() => {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// ── Populate Base URL dropdown options from preset.endpoint_variants ──────
|
|
175
|
+
// Mirrors settings.js _renderBaseUrlDropdown but for the single-shot
|
|
176
|
+
// onboarding combobox. Called whenever the provider changes so the dropdown
|
|
177
|
+
// reflects the currently selected preset (empty for Custom / presets with
|
|
178
|
+
// no variants — button stays inert in that case).
|
|
179
|
+
function _updateSetupBaseUrlDropdown(preset) {
|
|
180
|
+
const dd = $("setup-base-url-dropdown");
|
|
181
|
+
if (!dd) return;
|
|
182
|
+
dd.innerHTML = "";
|
|
183
|
+
|
|
184
|
+
const variants = preset && Array.isArray(preset.endpoint_variants)
|
|
185
|
+
? preset.endpoint_variants
|
|
186
|
+
: [];
|
|
187
|
+
|
|
188
|
+
if (variants.length === 0) {
|
|
189
|
+
// Leave dd empty; the dropdown-btn still toggles but shows a no-variant
|
|
190
|
+
// hint to signal "this provider has only one endpoint".
|
|
191
|
+
const empty = document.createElement("div");
|
|
192
|
+
empty.className = "model-dropdown-empty";
|
|
193
|
+
empty.textContent = I18n.t("settings.models.baseurl.noVariants");
|
|
194
|
+
dd.appendChild(empty);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
variants.forEach(v => {
|
|
199
|
+
// Prefer i18n key (localised per UI language); fall back to literal
|
|
200
|
+
// `label` (shipped English copy) then base_url for safety.
|
|
201
|
+
const translated = v.label_key ? I18n.t(v.label_key) : null;
|
|
202
|
+
const labelText = (translated && translated !== v.label_key) ? translated : (v.label || v.base_url);
|
|
203
|
+
|
|
204
|
+
const opt = document.createElement("div");
|
|
205
|
+
opt.className = "model-dropdown-option base-url-dropdown-option";
|
|
206
|
+
opt.dataset.value = v.base_url;
|
|
207
|
+
|
|
208
|
+
const lbl = document.createElement("div");
|
|
209
|
+
lbl.className = "base-url-dropdown-label";
|
|
210
|
+
lbl.textContent = labelText;
|
|
211
|
+
|
|
212
|
+
const url = document.createElement("div");
|
|
213
|
+
url.className = "base-url-dropdown-url";
|
|
214
|
+
url.textContent = v.base_url;
|
|
215
|
+
|
|
216
|
+
opt.appendChild(lbl);
|
|
217
|
+
opt.appendChild(url);
|
|
218
|
+
|
|
219
|
+
opt.addEventListener("click", (e) => {
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
$("setup-base-url").value = v.base_url;
|
|
222
|
+
dd.style.display = "none";
|
|
223
|
+
});
|
|
224
|
+
dd.appendChild(opt);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
174
228
|
function _bindCustomDropdown() {
|
|
175
229
|
if (_dropdownBound) return; // listeners already attached
|
|
176
230
|
_dropdownBound = true;
|
|
@@ -207,6 +261,7 @@ const Onboard = (() => {
|
|
|
207
261
|
$("setup-model").value = "";
|
|
208
262
|
$("setup-base-url").value = "";
|
|
209
263
|
_updateSetupModelDropdown([]);
|
|
264
|
+
_updateSetupBaseUrlDropdown(null);
|
|
210
265
|
if (getApiKeyLink) getApiKeyLink.style.display = "none";
|
|
211
266
|
} else if (value) {
|
|
212
267
|
const preset = _providers.find(p => p.id === value);
|
|
@@ -214,6 +269,7 @@ const Onboard = (() => {
|
|
|
214
269
|
$("setup-model").value = preset.default_model || "";
|
|
215
270
|
$("setup-base-url").value = preset.base_url || "";
|
|
216
271
|
_updateSetupModelDropdown(preset.models || []);
|
|
272
|
+
_updateSetupBaseUrlDropdown(preset);
|
|
217
273
|
// Show "how to get" link if provider has a website_url
|
|
218
274
|
if (getApiKeyLink && preset.website_url) {
|
|
219
275
|
getApiKeyLink.href = preset.website_url;
|
|
@@ -262,11 +318,30 @@ const Onboard = (() => {
|
|
|
262
318
|
modelDropdownBtn.addEventListener("click", (e) => {
|
|
263
319
|
e.stopPropagation();
|
|
264
320
|
const isOpen = modelDropdown.style.display === "block";
|
|
321
|
+
// Close sibling dropdown to avoid overlap.
|
|
322
|
+
$("setup-base-url-dropdown").style.display = "none";
|
|
265
323
|
modelDropdown.style.display = isOpen ? "none" : "block";
|
|
266
324
|
});
|
|
267
325
|
|
|
268
|
-
|
|
326
|
+
// ── Base URL combobox dropdown ────────────────────────────────────────────
|
|
327
|
+
// Shows preset.endpoint_variants (e.g. GLM mainland vs Z.ai international).
|
|
328
|
+
// Rendering is driven by the currently selected provider — see the
|
|
329
|
+
// provider custom-select handler above, which calls
|
|
330
|
+
// _updateSetupBaseUrlDropdown(preset) on every switch.
|
|
331
|
+
const baseUrlDropdownBtn = $("setup-base-url-dropdown-btn");
|
|
332
|
+
const baseUrlDropdown = $("setup-base-url-dropdown");
|
|
333
|
+
|
|
334
|
+
baseUrlDropdownBtn.addEventListener("click", (e) => {
|
|
335
|
+
e.stopPropagation();
|
|
336
|
+
const isOpen = baseUrlDropdown.style.display === "block";
|
|
337
|
+
// Close sibling dropdown (model combobox) to avoid overlap.
|
|
269
338
|
modelDropdown.style.display = "none";
|
|
339
|
+
baseUrlDropdown.style.display = isOpen ? "none" : "block";
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
document.addEventListener("click", () => {
|
|
343
|
+
modelDropdown.style.display = "none";
|
|
344
|
+
baseUrlDropdown.style.display = "none";
|
|
270
345
|
});
|
|
271
346
|
|
|
272
347
|
$("setup-btn-test").addEventListener("click", _testAndSave);
|
|
@@ -388,6 +463,7 @@ const Onboard = (() => {
|
|
|
388
463
|
// Boot the normal UI (WS + sessions sidebar + tasks + skills).
|
|
389
464
|
function _bootUI() {
|
|
390
465
|
document.body.classList.remove("setup-mode");
|
|
466
|
+
SkillAC.init();
|
|
391
467
|
WS.connect();
|
|
392
468
|
Tasks.load();
|
|
393
469
|
Skills.load();
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -2484,14 +2484,14 @@ const Sessions = (() => {
|
|
|
2484
2484
|
|
|
2485
2485
|
card.innerHTML = cardHtml;
|
|
2486
2486
|
|
|
2487
|
-
// Click → disable card + submit immediately via
|
|
2487
|
+
// Click → disable card + submit immediately via _sendMessage()
|
|
2488
2488
|
card.querySelectorAll(".feedback-option-btn").forEach(btn => {
|
|
2489
2489
|
btn.onclick = () => {
|
|
2490
2490
|
card.querySelectorAll(".feedback-option-btn").forEach(b => b.disabled = true);
|
|
2491
2491
|
card.classList.add("feedback-card--submitted");
|
|
2492
2492
|
const input = $("user-input");
|
|
2493
2493
|
if (input) input.value = btn.textContent.trim();
|
|
2494
|
-
|
|
2494
|
+
_sendMessage();
|
|
2495
2495
|
};
|
|
2496
2496
|
});
|
|
2497
2497
|
|
data/lib/clacky/web/settings.js
CHANGED
|
@@ -110,8 +110,17 @@ const Settings = (() => {
|
|
|
110
110
|
</label>
|
|
111
111
|
<label class="model-field">
|
|
112
112
|
<span class="field-label">${I18n.t("settings.models.field.baseurl")}</span>
|
|
113
|
-
<
|
|
114
|
-
|
|
113
|
+
<div class="base-url-combobox" data-index="${index}">
|
|
114
|
+
<input type="text" class="field-input base-url-input" data-key="base_url" data-index="${index}"
|
|
115
|
+
placeholder="${I18n.t("settings.models.placeholder.baseurl")}" value="${_esc(model.base_url)}"
|
|
116
|
+
autocomplete="off">
|
|
117
|
+
<button class="base-url-dropdown-btn" type="button" title="Select preset endpoint">
|
|
118
|
+
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
119
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
120
|
+
</svg>
|
|
121
|
+
</button>
|
|
122
|
+
<div class="base-url-dropdown" style="display:none"></div>
|
|
123
|
+
</div>
|
|
115
124
|
</label>
|
|
116
125
|
<label class="model-field">
|
|
117
126
|
<span class="field-label">
|
|
@@ -267,10 +276,21 @@ const Settings = (() => {
|
|
|
267
276
|
// Build model list from current base_url's provider
|
|
268
277
|
const _updateModelDropdown = () => {
|
|
269
278
|
const baseUrlInput = card.querySelector(`[data-key="base_url"]`);
|
|
270
|
-
const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : "";
|
|
271
|
-
|
|
272
|
-
// Find provider by base_url
|
|
273
|
-
|
|
279
|
+
const baseUrl = baseUrlInput ? baseUrlInput.value.trim().replace(/\/+$/, "") : "";
|
|
280
|
+
|
|
281
|
+
// Find provider by matching base_url against BOTH the canonical
|
|
282
|
+
// preset.base_url AND every endpoint_variants[].base_url — otherwise
|
|
283
|
+
// picking e.g. GLM's Coding-Plan variant would wipe the model list
|
|
284
|
+
// because only the canonical URL would match.
|
|
285
|
+
const provider = _providers.find(p => {
|
|
286
|
+
const candidates = [p.base_url].concat(
|
|
287
|
+
Array.isArray(p.endpoint_variants) ? p.endpoint_variants.map(v => v.base_url) : []
|
|
288
|
+
).filter(Boolean);
|
|
289
|
+
return candidates.some(c => {
|
|
290
|
+
const norm = String(c).replace(/\/+$/, "");
|
|
291
|
+
return baseUrl === norm || baseUrl.startsWith(norm + "/");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
274
294
|
const models = provider?.models || [];
|
|
275
295
|
|
|
276
296
|
if (models.length === 0) {
|
|
@@ -322,6 +342,107 @@ const Settings = (() => {
|
|
|
322
342
|
_updateModelDropdown();
|
|
323
343
|
});
|
|
324
344
|
}
|
|
345
|
+
|
|
346
|
+
// Base URL combobox: dropdown button + endpoint_variants list.
|
|
347
|
+
//
|
|
348
|
+
// Rationale: some providers (GLM on Zhipu/Z.ai, MiniMax on .com/.io) run
|
|
349
|
+
// multiple regional / billing-plan endpoints under a single identity.
|
|
350
|
+
// Listing every variant lets the user pick the right one instead of
|
|
351
|
+
// hand-editing the URL, while still allowing free-form input for
|
|
352
|
+
// unknown / self-hosted proxies. Mirrors the model-name combobox.
|
|
353
|
+
//
|
|
354
|
+
// Data source: the endpoint_variants[] field on each provider preset,
|
|
355
|
+
// resolved by matching the currently-entered base_url against every
|
|
356
|
+
// preset's {base_url + endpoint_variants[].base_url}. When no variants
|
|
357
|
+
// are declared for the matched provider (single-endpoint providers like
|
|
358
|
+
// Anthropic, OpenClacky), the dropdown shows an "empty" hint.
|
|
359
|
+
const baseUrlCombobox = card.querySelector(".base-url-combobox");
|
|
360
|
+
const baseUrlDropdownBtn = baseUrlCombobox.querySelector(".base-url-dropdown-btn");
|
|
361
|
+
const baseUrlDropdown = baseUrlCombobox.querySelector(".base-url-dropdown");
|
|
362
|
+
|
|
363
|
+
// Resolve the "active" provider preset from the current form values:
|
|
364
|
+
// 1. If the Quick Setup select points at a known provider, use that
|
|
365
|
+
// (even before the base_url input is typed into).
|
|
366
|
+
// 2. Otherwise fall back to matching the current base_url against all
|
|
367
|
+
// preset base_url + endpoint_variants. Unknown URLs → null.
|
|
368
|
+
const _currentProvider = () => {
|
|
369
|
+
const selected = card.querySelector(".custom-select-option.selected");
|
|
370
|
+
const selectedId = selected?.dataset.value;
|
|
371
|
+
if (selectedId && selectedId !== "custom") {
|
|
372
|
+
const byId = _providers.find(p => p.id === selectedId);
|
|
373
|
+
if (byId) return byId;
|
|
374
|
+
}
|
|
375
|
+
const url = (baseUrlInput?.value || "").trim().replace(/\/+$/, "");
|
|
376
|
+
if (!url) return null;
|
|
377
|
+
return _providers.find(p => {
|
|
378
|
+
const candidates = [p.base_url].concat(
|
|
379
|
+
Array.isArray(p.endpoint_variants) ? p.endpoint_variants.map(v => v.base_url) : []
|
|
380
|
+
).filter(Boolean);
|
|
381
|
+
return candidates.some(c => {
|
|
382
|
+
const norm = String(c).replace(/\/+$/, "");
|
|
383
|
+
return url === norm || url.startsWith(norm + "/");
|
|
384
|
+
});
|
|
385
|
+
}) || null;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const _renderBaseUrlDropdown = () => {
|
|
389
|
+
const provider = _currentProvider();
|
|
390
|
+
const variants = provider && Array.isArray(provider.endpoint_variants)
|
|
391
|
+
? provider.endpoint_variants
|
|
392
|
+
: [];
|
|
393
|
+
|
|
394
|
+
if (variants.length === 0) {
|
|
395
|
+
baseUrlDropdown.innerHTML =
|
|
396
|
+
`<div class="model-dropdown-empty">${I18n.t("settings.models.baseurl.noVariants")}</div>`;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
baseUrlDropdown.innerHTML = variants.map(v => {
|
|
401
|
+
// Prefer i18n key (localised per UI language); fall back to literal
|
|
402
|
+
// `label` (shipped English copy) and finally to base_url for safety.
|
|
403
|
+
// Pattern: _translateVariant(v) -> "大陆 · 按量付费" in zh, "Mainland · Pay-as-you-go" in en.
|
|
404
|
+
const translated = v.label_key ? I18n.t(v.label_key) : null;
|
|
405
|
+
// I18n.t typically returns the key itself when missing — treat that as a miss.
|
|
406
|
+
const labelText = (translated && translated !== v.label_key) ? translated : (v.label || v.base_url);
|
|
407
|
+
const label = _esc(labelText);
|
|
408
|
+
const url = _esc(v.base_url);
|
|
409
|
+
return `
|
|
410
|
+
<div class="model-dropdown-option base-url-dropdown-option" data-value="${url}">
|
|
411
|
+
<div class="base-url-dropdown-label">${label}</div>
|
|
412
|
+
<div class="base-url-dropdown-url">${url}</div>
|
|
413
|
+
</div>`;
|
|
414
|
+
}).join("");
|
|
415
|
+
|
|
416
|
+
baseUrlDropdown.querySelectorAll(".base-url-dropdown-option").forEach(opt => {
|
|
417
|
+
opt.addEventListener("click", (e) => {
|
|
418
|
+
e.stopPropagation();
|
|
419
|
+
if (baseUrlInput) {
|
|
420
|
+
baseUrlInput.value = opt.dataset.value;
|
|
421
|
+
// Trigger model-list refresh since base_url just changed.
|
|
422
|
+
_updateModelDropdown();
|
|
423
|
+
}
|
|
424
|
+
baseUrlDropdown.style.display = "none";
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
baseUrlDropdownBtn.addEventListener("click", (e) => {
|
|
430
|
+
e.stopPropagation();
|
|
431
|
+
const isOpen = baseUrlDropdown.style.display === "block";
|
|
432
|
+
// Close sibling dropdowns (model-name + other base-url) to avoid overlap.
|
|
433
|
+
document.querySelectorAll(".model-name-dropdown, .base-url-dropdown").forEach(d => {
|
|
434
|
+
d.style.display = "none";
|
|
435
|
+
});
|
|
436
|
+
if (!isOpen) {
|
|
437
|
+
_renderBaseUrlDropdown();
|
|
438
|
+
baseUrlDropdown.style.display = "block";
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Close dropdown when clicking outside
|
|
443
|
+
document.addEventListener("click", () => {
|
|
444
|
+
baseUrlDropdown.style.display = "none";
|
|
445
|
+
});
|
|
325
446
|
}
|
|
326
447
|
|
|
327
448
|
// ── Read form values from a card ────────────────────────────────────────────
|
data/lib/clacky/web/skills.js
CHANGED
|
@@ -782,6 +782,7 @@ const Skills = (() => {
|
|
|
782
782
|
// I18n, global $ helper.
|
|
783
783
|
// ─────────────────────────────────────────────────────────────────────────
|
|
784
784
|
const SkillAC = (() => {
|
|
785
|
+
let _initialized = false;
|
|
785
786
|
let _visible = false;
|
|
786
787
|
let _activeIndex = -1;
|
|
787
788
|
let _items = []; // filtered [{ name, description, encrypted, source }]
|
|
@@ -1175,6 +1176,9 @@ const SkillAC = (() => {
|
|
|
1175
1176
|
|
|
1176
1177
|
/** Initialize event listeners (call once on page load). */
|
|
1177
1178
|
init() {
|
|
1179
|
+
if (_initialized) return;
|
|
1180
|
+
_initialized = true;
|
|
1181
|
+
|
|
1178
1182
|
const chk = $("chk-ac-show-system-skills");
|
|
1179
1183
|
|
|
1180
1184
|
if (chk) {
|
data/lib/clacky.rb
CHANGED
|
@@ -133,6 +133,11 @@ module Clacky
|
|
|
133
133
|
class AgentError < StandardError; end
|
|
134
134
|
class BadRequestError < AgentError; end # 400 errors — our request was malformed, history should be rolled back
|
|
135
135
|
class RetryableError < StandardError; end # Transient errors that should be retried (5xx, HTML response, rate limit)
|
|
136
|
+
# Upstream (model/router like OpenRouter/Bedrock) returned finish_reason="stop" together with
|
|
137
|
+
# one or more tool_calls whose `arguments` JSON was truncated (empty, "{}" placeholder, or
|
|
138
|
+
# otherwise unparseable). Subclass of RetryableError so it flows through the existing
|
|
139
|
+
# retry/fallback pipeline in LlmCaller#call_llm.
|
|
140
|
+
class UpstreamTruncatedError < RetryableError; end
|
|
136
141
|
class ToolCallError < AgentError; end # Raised when tool call fails due to invalid parameters
|
|
137
142
|
class BrowserNotReachableError < AgentError; end # Chrome/Edge not running or remote debugging disabled
|
|
138
143
|
# BrowserManager singleton: Clacky::BrowserManager.instance
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -347,6 +347,7 @@ files:
|
|
|
347
347
|
- lib/clacky/default_skills/new/scripts/create_rails_project.sh
|
|
348
348
|
- lib/clacky/default_skills/onboard/SKILL.md
|
|
349
349
|
- lib/clacky/default_skills/onboard/scripts/import_external_skills.rb
|
|
350
|
+
- lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb
|
|
350
351
|
- lib/clacky/default_skills/personal-website/SKILL.md
|
|
351
352
|
- lib/clacky/default_skills/personal-website/publish.rb
|
|
352
353
|
- lib/clacky/default_skills/product-help/SKILL.md
|