openclacky 0.8.2 → 0.8.3
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 +13 -0
- data/lib/clacky/agent/session_serializer.rb +31 -0
- data/lib/clacky/agent/skill_manager.rb +41 -0
- data/lib/clacky/agent.rb +6 -1
- data/lib/clacky/agent_config.rb +10 -0
- data/lib/clacky/server/http_server.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +69 -0
- data/lib/clacky/web/sessions.js +78 -3
- data/lib/clacky/web/settings.js +64 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 473dceae1c35bc7eeb4095df2f35ea1403f6013168a3daf3318a709df4ce40ec
|
|
4
|
+
data.tar.gz: 9b7b97d917e13bf43ffff8fe50fda4f1eade74cd530e45959af46d9dbd313043
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6717eccea955e82d4dfa4381cb99707785d6ac36c3875ac622d51e854007d2997b16155af3e48d2b644aae3635d3989152a03e6f069bf525d8a8630049b6dfb0
|
|
7
|
+
data.tar.gz: 58cd3940c885363ae315e0430a318bb181713cdd0e5d1d4f7308343ce357f44075fd06cb8c4882f75824f857211c85c231b3d8b0cb2babe71ef6c76b5f21371c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.3] - 2026-03-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Slash command skill injection**: skill content is now injected as an assistant message for all `/skill-name` commands, giving the agent full context of the skill instructions at invocation time
|
|
14
|
+
- **Collapsible `<think>` blocks** in web UI: model reasoning enclosed in `<think>…</think>` tags is rendered as a collapsible "Thinking…" section instead of raw text
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
- **Web UI settings panel**: refined layout and styles for the settings modal
|
|
18
|
+
- **Session state restored on page refresh**: "Thinking…" progress indicator and error messages are now restored from session status after a page reload instead of disappearing
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **AgentConfig shallow-copy bug**: switching models in Settings no longer pollutes existing sessions — `deep_copy` (JSON round-trip) is now used everywhere instead of `dup` to prevent shared `@models` hash mutation across sessions
|
|
22
|
+
|
|
10
23
|
## [0.8.2] - 2026-03-09
|
|
11
24
|
|
|
12
25
|
### Added
|
|
@@ -52,6 +52,11 @@ module Clacky
|
|
|
52
52
|
})
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
# Rebuild and refresh the system prompt so any newly installed skills
|
|
57
|
+
# (or other configuration changes since the session was saved) are
|
|
58
|
+
# reflected immediately — without requiring the user to create a new session.
|
|
59
|
+
refresh_system_prompt
|
|
55
60
|
end
|
|
56
61
|
|
|
57
62
|
# Generate session data for saving
|
|
@@ -174,6 +179,10 @@ module Clacky
|
|
|
174
179
|
ui.show_user_message(display_text, created_at: msg[:created_at])
|
|
175
180
|
|
|
176
181
|
round[:events].each do |ev|
|
|
182
|
+
# Skip system-injected messages (e.g. synthetic skill content, memory prompts)
|
|
183
|
+
# — they are internal scaffolding and must not be shown to the user.
|
|
184
|
+
next if ev[:system_injected]
|
|
185
|
+
|
|
177
186
|
case ev[:role].to_s
|
|
178
187
|
when "assistant"
|
|
179
188
|
# Text content
|
|
@@ -210,6 +219,28 @@ module Clacky
|
|
|
210
219
|
|
|
211
220
|
private
|
|
212
221
|
|
|
222
|
+
# Replace the system message in @messages with a freshly built system prompt.
|
|
223
|
+
# Called after restore_session so newly installed skills and any other
|
|
224
|
+
# configuration changes since the session was saved take effect immediately.
|
|
225
|
+
# If no system message exists yet (shouldn't happen in practice), a new one
|
|
226
|
+
# is prepended so the conversation stays well-formed.
|
|
227
|
+
def refresh_system_prompt
|
|
228
|
+
# Reload skills from disk to pick up anything installed since the session was saved
|
|
229
|
+
@skill_loader.load_all
|
|
230
|
+
|
|
231
|
+
fresh_prompt = build_system_prompt
|
|
232
|
+
system_index = @messages.index { |m| m[:role] == "system" }
|
|
233
|
+
|
|
234
|
+
if system_index
|
|
235
|
+
@messages[system_index] = { role: "system", content: fresh_prompt }
|
|
236
|
+
else
|
|
237
|
+
@messages.unshift({ role: "system", content: fresh_prompt })
|
|
238
|
+
end
|
|
239
|
+
rescue StandardError => e
|
|
240
|
+
# Log and continue — a stale system prompt is better than a broken restore
|
|
241
|
+
Clacky::Logger.warn("refresh_system_prompt failed during session restore: #{e.message}")
|
|
242
|
+
end
|
|
243
|
+
|
|
213
244
|
# Extract text from message content (handles string and array formats)
|
|
214
245
|
# @param content [String, Array, Object] Message content
|
|
215
246
|
# @return [String] Extracted text
|
|
@@ -128,6 +128,47 @@ module Clacky
|
|
|
128
128
|
context
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# Inject a synthetic assistant message containing the skill content for slash
|
|
132
|
+
# commands (e.g. /pptx, /onboard).
|
|
133
|
+
#
|
|
134
|
+
# When a user types "/skill-name [arguments]", we immediately expand the skill
|
|
135
|
+
# content and inject it as an assistant message so the LLM receives the full
|
|
136
|
+
# instructions and acts on them — no waiting for the LLM to discover and call
|
|
137
|
+
# invoke_skill on its own.
|
|
138
|
+
#
|
|
139
|
+
# Message structure after injection:
|
|
140
|
+
# user: "/pptx write a deck about X"
|
|
141
|
+
# assistant: "[full skill content]" <- injected here
|
|
142
|
+
# (LLM continues from here)
|
|
143
|
+
#
|
|
144
|
+
# Fires when:
|
|
145
|
+
# 1. Input starts with "/"
|
|
146
|
+
# 2. The named skill exists and is user-invocable
|
|
147
|
+
#
|
|
148
|
+
# @param user_input [String] Raw user input
|
|
149
|
+
# @param task_id [Integer] Current task ID (for message tagging)
|
|
150
|
+
# @return [void]
|
|
151
|
+
def inject_skill_command_as_assistant_message(user_input, task_id)
|
|
152
|
+
parsed = parse_skill_command(user_input)
|
|
153
|
+
return unless parsed
|
|
154
|
+
|
|
155
|
+
skill = parsed[:skill]
|
|
156
|
+
arguments = parsed[:arguments]
|
|
157
|
+
|
|
158
|
+
# Expand skill content (substitutes $ARGUMENTS if present)
|
|
159
|
+
expanded_content = skill.process_content(arguments, template_context: build_template_context)
|
|
160
|
+
|
|
161
|
+
# Inject as a synthetic assistant message so the LLM treats it as already read
|
|
162
|
+
@messages << {
|
|
163
|
+
role: "assistant",
|
|
164
|
+
content: expanded_content,
|
|
165
|
+
task_id: task_id,
|
|
166
|
+
system_injected: true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@ui&.log("Injected skill content for /#{skill.identifier}", level: :info)
|
|
170
|
+
end
|
|
171
|
+
|
|
131
172
|
private
|
|
132
173
|
|
|
133
174
|
# Filter skills by the agent profile name using the skill's own `agent:` field.
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -177,6 +177,11 @@ module Clacky
|
|
|
177
177
|
@messages << { role: "user", content: user_content, task_id: task_id, created_at: Time.now.to_f }
|
|
178
178
|
@total_tasks += 1
|
|
179
179
|
|
|
180
|
+
# If the user typed a slash command targeting a skill with disable-model-invocation: true,
|
|
181
|
+
# inject the skill content as a synthetic assistant message so the LLM can act on it.
|
|
182
|
+
# Skills already in the system prompt (model_invocation_allowed?) are skipped.
|
|
183
|
+
inject_skill_command_as_assistant_message(user_input, task_id)
|
|
184
|
+
|
|
180
185
|
@hooks.trigger(:on_start, user_input)
|
|
181
186
|
|
|
182
187
|
begin
|
|
@@ -671,7 +676,7 @@ module Clacky
|
|
|
671
676
|
# @return [Agent] New subagent instance
|
|
672
677
|
def fork_subagent(model: nil, forbidden_tools: [], system_prompt_suffix: nil)
|
|
673
678
|
# Clone config to avoid affecting parent
|
|
674
|
-
subagent_config = @config.
|
|
679
|
+
subagent_config = @config.deep_copy
|
|
675
680
|
|
|
676
681
|
# Switch to specified model if provided
|
|
677
682
|
if model
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -226,6 +226,16 @@ module Clacky
|
|
|
226
226
|
end
|
|
227
227
|
|
|
228
228
|
# Save configuration to file
|
|
229
|
+
# Deep copy — models array contains mutable Hashes, so a shallow dup would
|
|
230
|
+
# let the copy share the same Hash objects with the original, causing
|
|
231
|
+
# Settings changes to silently mutate already-running session configs.
|
|
232
|
+
# JSON round-trip is the cleanest approach since @models is pure JSON-able data.
|
|
233
|
+
def deep_copy
|
|
234
|
+
copy = dup
|
|
235
|
+
copy.instance_variable_set(:@models, JSON.parse(JSON.generate(@models)))
|
|
236
|
+
copy
|
|
237
|
+
end
|
|
238
|
+
|
|
229
239
|
def save(config_file = CONFIG_FILE)
|
|
230
240
|
config_dir = File.dirname(config_file)
|
|
231
241
|
FileUtils.mkdir_p(config_dir)
|
|
@@ -1115,7 +1115,7 @@ module Clacky
|
|
|
1115
1115
|
session_id = @registry.create(name: name, working_dir: working_dir)
|
|
1116
1116
|
|
|
1117
1117
|
client = @client_factory.call
|
|
1118
|
-
config = @agent_config.
|
|
1118
|
+
config = @agent_config.deep_copy
|
|
1119
1119
|
config.permission_mode = permission_mode
|
|
1120
1120
|
broadcaster = method(:broadcast)
|
|
1121
1121
|
ui = WebUIController.new(session_id, broadcaster)
|
|
@@ -1142,7 +1142,7 @@ module Clacky
|
|
|
1142
1142
|
session_id: original_id)
|
|
1143
1143
|
|
|
1144
1144
|
client = @client_factory.call
|
|
1145
|
-
config = @agent_config.
|
|
1145
|
+
config = @agent_config.deep_copy
|
|
1146
1146
|
broadcaster = method(:broadcast)
|
|
1147
1147
|
ui = WebUIController.new(session_id, broadcaster)
|
|
1148
1148
|
agent = Clacky::Agent.from_session(client, config, session_data, ui: ui, profile: "general")
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -401,6 +401,7 @@ body {
|
|
|
401
401
|
#chat-title { font-weight: 600; font-size: 15px; }
|
|
402
402
|
.status-idle { font-size: 11px; color: #8b949e; }
|
|
403
403
|
.status-running { font-size: 11px; color: #3fb950; }
|
|
404
|
+
.status-error { font-size: 11px; color: #f85149; }
|
|
404
405
|
|
|
405
406
|
/* Delete session button in chat header */
|
|
406
407
|
.btn-delete-session {
|
|
@@ -500,6 +501,52 @@ body {
|
|
|
500
501
|
.tool-item-status.err { color: #f85149; }
|
|
501
502
|
.tool-item-status.running { color: #58a6ff; animation: pulse 1.2s infinite; }
|
|
502
503
|
|
|
504
|
+
/* ── Thinking block (collapsible <think>...</think> sections) ─────────────── */
|
|
505
|
+
.thinking-block {
|
|
506
|
+
margin: 0 0 6px 0;
|
|
507
|
+
background: transparent;
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.thinking-summary {
|
|
512
|
+
display: inline-flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
gap: 5px;
|
|
515
|
+
padding: 0;
|
|
516
|
+
cursor: pointer;
|
|
517
|
+
user-select: none;
|
|
518
|
+
list-style: none;
|
|
519
|
+
color: #6e7681;
|
|
520
|
+
font-size: 12px;
|
|
521
|
+
font-style: italic;
|
|
522
|
+
transition: color 0.15s;
|
|
523
|
+
}
|
|
524
|
+
.thinking-summary::-webkit-details-marker { display: none; }
|
|
525
|
+
.thinking-summary:hover { color: #8b949e; }
|
|
526
|
+
|
|
527
|
+
.thinking-icon { display: none; }
|
|
528
|
+
.thinking-label { font-weight: 400; }
|
|
529
|
+
.thinking-chevron {
|
|
530
|
+
font-size: 12px;
|
|
531
|
+
color: inherit;
|
|
532
|
+
transition: transform 0.2s;
|
|
533
|
+
line-height: 1;
|
|
534
|
+
display: inline-block;
|
|
535
|
+
}
|
|
536
|
+
.thinking-block[open] .thinking-chevron { transform: rotate(90deg); }
|
|
537
|
+
|
|
538
|
+
.thinking-body {
|
|
539
|
+
margin-top: 2px;
|
|
540
|
+
margin-bottom: 10px;
|
|
541
|
+
padding-left: 14px;
|
|
542
|
+
font-size: 12px;
|
|
543
|
+
line-height: 1.5;
|
|
544
|
+
color: #6e7681;
|
|
545
|
+
white-space: pre-wrap;
|
|
546
|
+
word-break: break-word;
|
|
547
|
+
font-style: italic;
|
|
548
|
+
}
|
|
549
|
+
|
|
503
550
|
/* Inline image thumbnails inside user message bubbles */
|
|
504
551
|
.msg-image-thumb {
|
|
505
552
|
display: block;
|
|
@@ -828,6 +875,7 @@ body {
|
|
|
828
875
|
display: flex;
|
|
829
876
|
flex-direction: column;
|
|
830
877
|
gap: 14px;
|
|
878
|
+
margin-bottom: 16px;
|
|
831
879
|
}
|
|
832
880
|
.model-card-header {
|
|
833
881
|
display: flex;
|
|
@@ -958,6 +1006,27 @@ body {
|
|
|
958
1006
|
font-size: 13px;
|
|
959
1007
|
padding: 6px 18px;
|
|
960
1008
|
}
|
|
1009
|
+
.btn-set-default {
|
|
1010
|
+
font-size: 13px;
|
|
1011
|
+
padding: 6px 14px;
|
|
1012
|
+
background: #238636;
|
|
1013
|
+
color: #fff;
|
|
1014
|
+
border: none;
|
|
1015
|
+
border-radius: 6px;
|
|
1016
|
+
cursor: pointer;
|
|
1017
|
+
}
|
|
1018
|
+
.btn-set-default:hover {
|
|
1019
|
+
background: #2ea043;
|
|
1020
|
+
}
|
|
1021
|
+
.btn-set-default:disabled {
|
|
1022
|
+
background: #484f58;
|
|
1023
|
+
cursor: not-allowed;
|
|
1024
|
+
}
|
|
1025
|
+
.model-card-actions-row {
|
|
1026
|
+
display: flex;
|
|
1027
|
+
gap: 8px;
|
|
1028
|
+
align-items: center;
|
|
1029
|
+
}
|
|
961
1030
|
|
|
962
1031
|
/* ── Modals ──────────────────────────────────────────────────────────────── */
|
|
963
1032
|
.modal-overlay {
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -23,6 +23,59 @@ const Sessions = (() => {
|
|
|
23
23
|
let _pendingRunTaskId = null; // session_id waiting to send "run_task" after subscribe
|
|
24
24
|
let _pendingMessage = null; // { session_id, content } — slash command to send after subscribe
|
|
25
25
|
|
|
26
|
+
// ── Thinking block parser ──────────────────────────────────────────────
|
|
27
|
+
//
|
|
28
|
+
// Converts <think>...</think> blocks in assistant messages into collapsible
|
|
29
|
+
// "Thinking" sections. The block is collapsed by default and can be
|
|
30
|
+
// expanded by clicking the header.
|
|
31
|
+
|
|
32
|
+
function _parseThinkingBlocks(rawHtml) {
|
|
33
|
+
// rawHtml is already HTML-escaped text (via escapeHtml). We need to detect
|
|
34
|
+
// the escaped versions of <think> and </think>.
|
|
35
|
+
const OPEN = "<think>";
|
|
36
|
+
const CLOSE = "</think>";
|
|
37
|
+
|
|
38
|
+
// Fast path: no thinking block present
|
|
39
|
+
if (!rawHtml.includes(OPEN)) return rawHtml;
|
|
40
|
+
|
|
41
|
+
let result = "";
|
|
42
|
+
let rest = rawHtml;
|
|
43
|
+
|
|
44
|
+
while (rest.includes(OPEN)) {
|
|
45
|
+
const openIdx = rest.indexOf(OPEN);
|
|
46
|
+
const closeIdx = rest.indexOf(CLOSE, openIdx + OPEN.length);
|
|
47
|
+
|
|
48
|
+
// Prepend any text before the <think> block
|
|
49
|
+
result += rest.slice(0, openIdx);
|
|
50
|
+
|
|
51
|
+
if (closeIdx === -1) {
|
|
52
|
+
// Unclosed <think> — treat remainder as plain text
|
|
53
|
+
result += rest.slice(openIdx);
|
|
54
|
+
rest = "";
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const thinkContent = rest.slice(openIdx + OPEN.length, closeIdx);
|
|
59
|
+
result += _buildThinkingBlock(thinkContent);
|
|
60
|
+
// Strip leading newlines after </think> to avoid blank space from pre-wrap
|
|
61
|
+
rest = rest.slice(closeIdx + CLOSE.length).replace(/^\n+/, "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
result += rest;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build the collapsible thinking block HTML for a given (already-escaped) content string.
|
|
69
|
+
function _buildThinkingBlock(escapedContent) {
|
|
70
|
+
return `<details class="thinking-block">` +
|
|
71
|
+
`<summary class="thinking-summary">` +
|
|
72
|
+
`<span class="thinking-chevron">›</span>` +
|
|
73
|
+
`<span class="thinking-label">Thought for a moment</span>` +
|
|
74
|
+
`</summary>` +
|
|
75
|
+
`<div class="thinking-body">${escapedContent}</div>` +
|
|
76
|
+
`</details>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
26
79
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
27
80
|
|
|
28
81
|
function _cacheActiveMessages() {
|
|
@@ -151,7 +204,7 @@ const Sessions = (() => {
|
|
|
151
204
|
if (historyCtx.group) { _collapseToolGroup(historyCtx.group); historyCtx.group = null; }
|
|
152
205
|
const el = document.createElement("div");
|
|
153
206
|
el.className = "msg msg-assistant";
|
|
154
|
-
el.innerHTML = escapeHtml(ev.content || "");
|
|
207
|
+
el.innerHTML = _parseThinkingBlocks(escapeHtml(ev.content || ""));
|
|
155
208
|
container.appendChild(el);
|
|
156
209
|
break;
|
|
157
210
|
}
|
|
@@ -241,6 +294,21 @@ const Sessions = (() => {
|
|
|
241
294
|
messages.appendChild(frag);
|
|
242
295
|
messages.scrollTop = messages.scrollHeight;
|
|
243
296
|
}
|
|
297
|
+
|
|
298
|
+
// Restore transient UI state based on session status after initial load
|
|
299
|
+
// (not prepend, which is scroll-up pagination — no need to re-restore then)
|
|
300
|
+
if (!prepend) {
|
|
301
|
+
const session = _sessions.find(s => s.id === id);
|
|
302
|
+
if (session) {
|
|
303
|
+
if (session.status === "running") {
|
|
304
|
+
// Agent is still running (e.g. page was refreshed mid-task)
|
|
305
|
+
Sessions.showProgress("Thinking…");
|
|
306
|
+
} else if (session.status === "error" && session.error) {
|
|
307
|
+
// Show the stored error message at the end of history
|
|
308
|
+
Sessions.appendMsg("error", session.error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
244
312
|
}
|
|
245
313
|
} finally {
|
|
246
314
|
state.loading = false;
|
|
@@ -401,7 +469,13 @@ const Sessions = (() => {
|
|
|
401
469
|
|
|
402
470
|
updateStatusBar(status) {
|
|
403
471
|
$("chat-status").textContent = status || "idle";
|
|
404
|
-
|
|
472
|
+
if (status === "running") {
|
|
473
|
+
$("chat-status").className = "status-running";
|
|
474
|
+
} else if (status === "error") {
|
|
475
|
+
$("chat-status").className = "status-error";
|
|
476
|
+
} else {
|
|
477
|
+
$("chat-status").className = "status-idle";
|
|
478
|
+
}
|
|
405
479
|
$("btn-interrupt").style.display = status === "running" ? "" : "none";
|
|
406
480
|
},
|
|
407
481
|
|
|
@@ -447,7 +521,8 @@ const Sessions = (() => {
|
|
|
447
521
|
const messages = $("messages");
|
|
448
522
|
const el = document.createElement("div");
|
|
449
523
|
el.className = `msg msg-${type}`;
|
|
450
|
-
|
|
524
|
+
// Parse thinking blocks out of assistant messages
|
|
525
|
+
el.innerHTML = type === "assistant" ? _parseThinkingBlocks(html) : html;
|
|
451
526
|
messages.appendChild(el);
|
|
452
527
|
messages.scrollTop = messages.scrollHeight;
|
|
453
528
|
},
|
data/lib/clacky/web/settings.js
CHANGED
|
@@ -107,7 +107,10 @@ const Settings = (() => {
|
|
|
107
107
|
|
|
108
108
|
<div class="model-card-footer">
|
|
109
109
|
<span class="model-test-result" data-index="${index}"></span>
|
|
110
|
-
<
|
|
110
|
+
<div class="model-card-actions-row">
|
|
111
|
+
${!isDefault ? `<button class="btn-set-default" data-index="${index}" title="Set as default model">Set as Default</button>` : ""}
|
|
112
|
+
<button class="btn-save-model btn-primary" data-index="${index}">Save</button>
|
|
113
|
+
</div>
|
|
111
114
|
</div>
|
|
112
115
|
`;
|
|
113
116
|
|
|
@@ -143,6 +146,12 @@ const Settings = (() => {
|
|
|
143
146
|
if (removeBtn) {
|
|
144
147
|
removeBtn.addEventListener("click", () => _removeModel(index));
|
|
145
148
|
}
|
|
149
|
+
|
|
150
|
+
// Set as default model
|
|
151
|
+
const setDefaultBtn = card.querySelector(".btn-set-default");
|
|
152
|
+
if (setDefaultBtn) {
|
|
153
|
+
setDefaultBtn.addEventListener("click", () => _setAsDefault(index));
|
|
154
|
+
}
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
// ── Read form values from a card ────────────────────────────────────────────
|
|
@@ -233,16 +242,69 @@ const Settings = (() => {
|
|
|
233
242
|
el.className = `model-test-result ${ok ? "result-ok" : "result-fail"}`;
|
|
234
243
|
}
|
|
235
244
|
|
|
245
|
+
// ── Set as Default Model ───────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
async function _setAsDefault(index) {
|
|
248
|
+
const btn = document.querySelector(`.btn-set-default[data-index="${index}"]`);
|
|
249
|
+
if (!btn) return;
|
|
250
|
+
|
|
251
|
+
btn.disabled = true;
|
|
252
|
+
btn.textContent = "Setting…";
|
|
253
|
+
|
|
254
|
+
// Set the selected one as "default", others as null (not just delete)
|
|
255
|
+
// Using null ensures the server explicitly updates/removes the type field
|
|
256
|
+
const updatedModels = _models.map((m, i) => {
|
|
257
|
+
const model = { ...m };
|
|
258
|
+
if (i === index) {
|
|
259
|
+
model.type = "default";
|
|
260
|
+
} else {
|
|
261
|
+
model.type = null;
|
|
262
|
+
}
|
|
263
|
+
return model;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const res = await fetch("/api/config", {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: { "Content-Type": "application/json" },
|
|
270
|
+
body: JSON.stringify({ models: updatedModels })
|
|
271
|
+
});
|
|
272
|
+
const data = await res.json();
|
|
273
|
+
|
|
274
|
+
if (data.ok) {
|
|
275
|
+
btn.textContent = "Done ✓";
|
|
276
|
+
// Reload to refresh the UI
|
|
277
|
+
setTimeout(_load, 800);
|
|
278
|
+
} else {
|
|
279
|
+
btn.textContent = "Set as Default";
|
|
280
|
+
btn.disabled = false;
|
|
281
|
+
alert(data.error || "Failed to set default model");
|
|
282
|
+
}
|
|
283
|
+
} catch (e) {
|
|
284
|
+
btn.textContent = "Set as Default";
|
|
285
|
+
btn.disabled = false;
|
|
286
|
+
alert("Error: " + e.message);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
236
290
|
// ── Add / Remove model ───────────────────────────────────────────────────────
|
|
237
291
|
|
|
238
292
|
function _addModel() {
|
|
293
|
+
// When adding a new model, automatically set it as default.
|
|
294
|
+
// Set all existing models' type to null (not just delete) so server updates them.
|
|
295
|
+
_models = _models.map(m => {
|
|
296
|
+
const model = { ...m };
|
|
297
|
+
model.type = null;
|
|
298
|
+
return model;
|
|
299
|
+
});
|
|
300
|
+
|
|
239
301
|
_models.push({
|
|
240
302
|
index: _models.length,
|
|
241
303
|
model: "",
|
|
242
304
|
base_url: "",
|
|
243
305
|
api_key_masked: "",
|
|
244
306
|
anthropic_format: false,
|
|
245
|
-
type:
|
|
307
|
+
type: "default" // New model automatically becomes default
|
|
246
308
|
});
|
|
247
309
|
_renderCards();
|
|
248
310
|
// Scroll to the new card
|