openclacky 0.8.2 → 0.8.4
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 +34 -0
- data/lib/clacky/agent/session_serializer.rb +31 -0
- data/lib/clacky/agent/skill_manager.rb +59 -0
- data/lib/clacky/agent.rb +7 -2
- data/lib/clacky/agent_config.rb +10 -0
- data/lib/clacky/brand_config.rb +111 -24
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/server/http_server.rb +7 -4
- data/lib/clacky/skill_loader.rb +22 -18
- data/lib/clacky/ui2/layout_manager.rb +5 -0
- data/lib/clacky/ui2/screen_buffer.rb +24 -7
- data/lib/clacky/ui2/ui_controller.rb +56 -19
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +947 -337
- data/lib/clacky/web/app.js +30 -11
- data/lib/clacky/web/index.html +108 -30
- data/lib/clacky/web/onboard.js +92 -16
- data/lib/clacky/web/sessions.js +78 -3
- data/lib/clacky/web/settings.js +179 -26
- data/lib/clacky/web/skills.js +7 -3
- data/lib/clacky/web/tasks.js +34 -8
- data/lib/clacky/web/theme.js +67 -0
- metadata +16 -1
data/lib/clacky/web/settings.js
CHANGED
|
@@ -71,7 +71,7 @@ const Settings = (() => {
|
|
|
71
71
|
</div>
|
|
72
72
|
<div class="model-card-actions">
|
|
73
73
|
${_models.length > 1
|
|
74
|
-
? `<button class="btn-model-remove" data-index="${index}" title="Remove this model"
|
|
74
|
+
? `<button class="btn-model-remove" data-index="${index}" title="Remove this model">×</button>`
|
|
75
75
|
: ""}
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|
|
@@ -79,11 +79,19 @@ const Settings = (() => {
|
|
|
79
79
|
<div class="model-fields">
|
|
80
80
|
<label class="model-field">
|
|
81
81
|
<span class="field-label">Quick Setup</span>
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
<div class="custom-select-wrapper" data-index="${index}">
|
|
83
|
+
<div class="custom-select-trigger">
|
|
84
|
+
<span class="custom-select-value placeholder">— Choose provider —</span>
|
|
85
|
+
<svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
86
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
87
|
+
</svg>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="custom-select-dropdown">
|
|
90
|
+
<div class="custom-select-option" data-value="">— Choose provider —</div>
|
|
91
|
+
${_providers.map(p => `<div class="custom-select-option" data-value="${p.id}">${_esc(p.name)}</div>`).join("")}
|
|
92
|
+
<div class="custom-select-option" data-value="custom">Custom</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
87
95
|
</label>
|
|
88
96
|
<label class="model-field">
|
|
89
97
|
<span class="field-label">Model</span>
|
|
@@ -100,14 +108,22 @@ const Settings = (() => {
|
|
|
100
108
|
<div class="field-input-row">
|
|
101
109
|
<input type="password" class="field-input api-key-input" data-key="api_key" data-index="${index}"
|
|
102
110
|
placeholder="sk-…" value="${_esc(model.api_key_masked)}">
|
|
103
|
-
<button class="btn-toggle-key" data-index="${index}" title="Show/hide key"
|
|
111
|
+
<button class="btn-toggle-key" data-index="${index}" title="Show/hide key">
|
|
112
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
113
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
114
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
115
|
+
</svg>
|
|
116
|
+
</button>
|
|
104
117
|
</div>
|
|
105
118
|
</label>
|
|
106
119
|
</div>
|
|
107
120
|
|
|
108
121
|
<div class="model-card-footer">
|
|
109
122
|
<span class="model-test-result" data-index="${index}"></span>
|
|
110
|
-
<
|
|
123
|
+
<div class="model-card-actions-row">
|
|
124
|
+
${!isDefault ? `<button class="btn-set-default" data-index="${index}" title="Set as default model">Set as Default</button>` : ""}
|
|
125
|
+
<button class="btn-save-model btn-primary" data-index="${index}">Save</button>
|
|
126
|
+
</div>
|
|
111
127
|
</div>
|
|
112
128
|
`;
|
|
113
129
|
|
|
@@ -116,23 +132,92 @@ const Settings = (() => {
|
|
|
116
132
|
}
|
|
117
133
|
|
|
118
134
|
function _bindCardEvents(card, index) {
|
|
119
|
-
//
|
|
120
|
-
card.querySelector(".
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
// Custom dropdown interactions
|
|
136
|
+
const customSelectWrapper = card.querySelector(".custom-select-wrapper");
|
|
137
|
+
const trigger = customSelectWrapper.querySelector(".custom-select-trigger");
|
|
138
|
+
const dropdown = customSelectWrapper.querySelector(".custom-select-dropdown");
|
|
139
|
+
const valueSpan = trigger.querySelector(".custom-select-value");
|
|
140
|
+
const options = dropdown.querySelectorAll(".custom-select-option");
|
|
141
|
+
|
|
142
|
+
// Toggle dropdown
|
|
143
|
+
trigger.addEventListener("click", (e) => {
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
const isOpen = dropdown.classList.contains("open");
|
|
146
|
+
// Close all other dropdowns
|
|
147
|
+
document.querySelectorAll(".custom-select-dropdown.open").forEach(d => {
|
|
148
|
+
d.classList.remove("open");
|
|
149
|
+
d.previousElementSibling.classList.remove("open");
|
|
150
|
+
});
|
|
151
|
+
if (!isOpen) {
|
|
152
|
+
dropdown.classList.add("open");
|
|
153
|
+
trigger.classList.add("open");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Select option
|
|
158
|
+
options.forEach(option => {
|
|
159
|
+
option.addEventListener("click", (e) => {
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
const value = option.dataset.value;
|
|
162
|
+
const text = option.textContent;
|
|
163
|
+
|
|
164
|
+
// Update UI
|
|
165
|
+
valueSpan.textContent = text;
|
|
166
|
+
if (value) {
|
|
167
|
+
valueSpan.classList.remove("placeholder");
|
|
168
|
+
} else {
|
|
169
|
+
valueSpan.classList.add("placeholder");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Update selected state
|
|
173
|
+
options.forEach(opt => opt.classList.remove("selected"));
|
|
174
|
+
option.classList.add("selected");
|
|
175
|
+
|
|
176
|
+
// Close dropdown
|
|
177
|
+
dropdown.classList.remove("open");
|
|
178
|
+
trigger.classList.remove("open");
|
|
179
|
+
|
|
180
|
+
// Auto-fill model & base_url if a provider preset was selected
|
|
181
|
+
if (value && value !== "custom") {
|
|
182
|
+
const preset = _providers.find(p => p.id === value);
|
|
183
|
+
if (preset) {
|
|
184
|
+
const modelInput = card.querySelector(`[data-key="model"]`);
|
|
185
|
+
const baseUrlInput = card.querySelector(`[data-key="base_url"]`);
|
|
186
|
+
if (modelInput) modelInput.value = preset.default_model || "";
|
|
187
|
+
if (baseUrlInput) baseUrlInput.value = preset.base_url || "";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Close dropdown when clicking outside
|
|
194
|
+
document.addEventListener("click", () => {
|
|
195
|
+
dropdown.classList.remove("open");
|
|
196
|
+
trigger.classList.remove("open");
|
|
130
197
|
});
|
|
131
198
|
|
|
132
199
|
// Toggle API key visibility
|
|
133
|
-
card.querySelector(".btn-toggle-key")
|
|
134
|
-
|
|
135
|
-
|
|
200
|
+
const toggleKeyBtn = card.querySelector(".btn-toggle-key");
|
|
201
|
+
const apiKeyInput = card.querySelector(".api-key-input");
|
|
202
|
+
const eyeIcon = toggleKeyBtn.querySelector("svg");
|
|
203
|
+
|
|
204
|
+
toggleKeyBtn.addEventListener("click", () => {
|
|
205
|
+
const isPassword = apiKeyInput.type === "password";
|
|
206
|
+
apiKeyInput.type = isPassword ? "text" : "password";
|
|
207
|
+
|
|
208
|
+
// Update icon
|
|
209
|
+
if (isPassword) {
|
|
210
|
+
// Show eye-off icon
|
|
211
|
+
eyeIcon.innerHTML = `
|
|
212
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
|
|
213
|
+
`;
|
|
214
|
+
} else {
|
|
215
|
+
// Show eye icon
|
|
216
|
+
eyeIcon.innerHTML = `
|
|
217
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
218
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
136
221
|
});
|
|
137
222
|
|
|
138
223
|
// Save: auto-test first, then save if passed
|
|
@@ -143,6 +228,12 @@ const Settings = (() => {
|
|
|
143
228
|
if (removeBtn) {
|
|
144
229
|
removeBtn.addEventListener("click", () => _removeModel(index));
|
|
145
230
|
}
|
|
231
|
+
|
|
232
|
+
// Set as default model
|
|
233
|
+
const setDefaultBtn = card.querySelector(".btn-set-default");
|
|
234
|
+
if (setDefaultBtn) {
|
|
235
|
+
setDefaultBtn.addEventListener("click", () => _setAsDefault(index));
|
|
236
|
+
}
|
|
146
237
|
}
|
|
147
238
|
|
|
148
239
|
// ── Read form values from a card ────────────────────────────────────────────
|
|
@@ -233,26 +324,88 @@ const Settings = (() => {
|
|
|
233
324
|
el.className = `model-test-result ${ok ? "result-ok" : "result-fail"}`;
|
|
234
325
|
}
|
|
235
326
|
|
|
327
|
+
// ── Set as Default Model ───────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
async function _setAsDefault(index) {
|
|
330
|
+
const btn = document.querySelector(`.btn-set-default[data-index="${index}"]`);
|
|
331
|
+
if (!btn) return;
|
|
332
|
+
|
|
333
|
+
btn.disabled = true;
|
|
334
|
+
btn.textContent = "Setting…";
|
|
335
|
+
|
|
336
|
+
// Set the selected one as "default", others as null (not just delete)
|
|
337
|
+
// Using null ensures the server explicitly updates/removes the type field
|
|
338
|
+
const updatedModels = _models.map((m, i) => {
|
|
339
|
+
const model = { ...m };
|
|
340
|
+
if (i === index) {
|
|
341
|
+
model.type = "default";
|
|
342
|
+
} else {
|
|
343
|
+
model.type = null;
|
|
344
|
+
}
|
|
345
|
+
return model;
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const res = await fetch("/api/config", {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "Content-Type": "application/json" },
|
|
352
|
+
body: JSON.stringify({ models: updatedModels })
|
|
353
|
+
});
|
|
354
|
+
const data = await res.json();
|
|
355
|
+
|
|
356
|
+
if (data.ok) {
|
|
357
|
+
btn.textContent = "Done ✓";
|
|
358
|
+
// Reload to refresh the UI
|
|
359
|
+
setTimeout(_load, 800);
|
|
360
|
+
} else {
|
|
361
|
+
btn.textContent = "Set as Default";
|
|
362
|
+
btn.disabled = false;
|
|
363
|
+
alert(data.error || "Failed to set default model");
|
|
364
|
+
}
|
|
365
|
+
} catch (e) {
|
|
366
|
+
btn.textContent = "Set as Default";
|
|
367
|
+
btn.disabled = false;
|
|
368
|
+
alert("Error: " + e.message);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
236
372
|
// ── Add / Remove model ───────────────────────────────────────────────────────
|
|
237
373
|
|
|
238
374
|
function _addModel() {
|
|
375
|
+
// When adding a new model, automatically set it as default.
|
|
376
|
+
// Set all existing models' type to null (not just delete) so server updates them.
|
|
377
|
+
_models = _models.map(m => {
|
|
378
|
+
const model = { ...m };
|
|
379
|
+
model.type = null;
|
|
380
|
+
return model;
|
|
381
|
+
});
|
|
382
|
+
|
|
239
383
|
_models.push({
|
|
240
384
|
index: _models.length,
|
|
241
385
|
model: "",
|
|
242
386
|
base_url: "",
|
|
243
387
|
api_key_masked: "",
|
|
244
388
|
anthropic_format: false,
|
|
245
|
-
type:
|
|
389
|
+
type: "default" // New model automatically becomes default
|
|
246
390
|
});
|
|
247
391
|
_renderCards();
|
|
248
|
-
// Scroll to the new card
|
|
392
|
+
// Scroll to the new card with offset
|
|
249
393
|
const cards = document.querySelectorAll(".model-card");
|
|
250
|
-
if (cards.length)
|
|
394
|
+
if (cards.length) {
|
|
395
|
+
const lastCard = cards[cards.length - 1];
|
|
396
|
+
lastCard.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
397
|
+
// Add 20px offset after scroll completes
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
const container = document.getElementById("settings-body");
|
|
400
|
+
if (container) container.scrollTop -= 20;
|
|
401
|
+
}, 300);
|
|
402
|
+
}
|
|
251
403
|
}
|
|
252
404
|
|
|
253
405
|
async function _removeModel(index) {
|
|
254
406
|
if (_models.length <= 1) return;
|
|
255
|
-
|
|
407
|
+
const confirmed = await Modal.confirm(`Remove model "${_models[index]?.model || index + 1}"?`);
|
|
408
|
+
if (!confirmed) return;
|
|
256
409
|
|
|
257
410
|
_models.splice(index, 1);
|
|
258
411
|
|
data/lib/clacky/web/skills.js
CHANGED
|
@@ -352,9 +352,13 @@ const Skills = (() => {
|
|
|
352
352
|
const labelEl = $("skills-sidebar-label");
|
|
353
353
|
if (!labelEl) return;
|
|
354
354
|
const total = _skills.length;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
// Don't show "No skills" when the skills panel is active
|
|
356
|
+
const isActive = Router.current === "skills";
|
|
357
|
+
if (total === 0) {
|
|
358
|
+
labelEl.textContent = isActive ? "Skills" : "No skills";
|
|
359
|
+
} else {
|
|
360
|
+
labelEl.textContent = `${total} skill${total !== 1 ? "s" : ""}`;
|
|
361
|
+
}
|
|
358
362
|
},
|
|
359
363
|
|
|
360
364
|
// ── Actions ───────────────────────────────────────────────────────────
|
data/lib/clacky/web/tasks.js
CHANGED
|
@@ -47,15 +47,32 @@ const Tasks = (() => {
|
|
|
47
47
|
|
|
48
48
|
row.innerHTML = `
|
|
49
49
|
<div class="task-col task-col-name">
|
|
50
|
-
<
|
|
50
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="task-icon">
|
|
51
|
+
<circle cx="12" cy="12" r="10"/>
|
|
52
|
+
<polyline points="12 6 12 12 16 14"/>
|
|
53
|
+
</svg>
|
|
51
54
|
<span class="task-name-text">${escapeHtml(t.name)}</span>
|
|
52
55
|
</div>
|
|
53
56
|
<div class="task-col task-col-schedule">${schedLabel}</div>
|
|
54
57
|
<div class="task-col task-col-content">${previewText}</div>
|
|
55
58
|
<div class="task-col task-col-actions">
|
|
56
|
-
<button class="task-btn task-btn-run" title="Run now"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
<button class="task-btn task-btn-run" title="Run now">
|
|
60
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
61
|
+
<polygon points="6 3 20 12 6 21 6 3"/>
|
|
62
|
+
</svg> Run
|
|
63
|
+
</button>
|
|
64
|
+
<button class="task-btn task-btn-edit" title="Edit task">
|
|
65
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
66
|
+
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
|
67
|
+
<path d="m15 5 4 4"/>
|
|
68
|
+
</svg> Edit
|
|
69
|
+
</button>
|
|
70
|
+
<button class="task-btn task-btn-del" title="Delete">
|
|
71
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
72
|
+
<path d="M18 6 6 18"/>
|
|
73
|
+
<path d="m6 6 12 12"/>
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
59
76
|
</div>`;
|
|
60
77
|
|
|
61
78
|
row.querySelector(".task-btn-run").addEventListener("click", e => {
|
|
@@ -113,9 +130,13 @@ const Tasks = (() => {
|
|
|
113
130
|
// Sidebar item is static in HTML — just update the label text.
|
|
114
131
|
const labelEl = $("tasks-sidebar-label");
|
|
115
132
|
if (!labelEl) return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
// Don't show "No tasks" when the tasks panel is active
|
|
134
|
+
const isActive = Router.current === "tasks";
|
|
135
|
+
if (_tasks.length === 0) {
|
|
136
|
+
labelEl.textContent = isActive ? "Tasks" : "No tasks";
|
|
137
|
+
} else {
|
|
138
|
+
labelEl.textContent = `${_tasks.length} task${_tasks.length !== 1 ? "s" : ""}`;
|
|
139
|
+
}
|
|
119
140
|
},
|
|
120
141
|
|
|
121
142
|
// ── Main panel table ──────────────────────────────────────────────────
|
|
@@ -130,7 +151,12 @@ const Tasks = (() => {
|
|
|
130
151
|
empty.className = "task-table-empty";
|
|
131
152
|
empty.innerHTML = `
|
|
132
153
|
<p>No scheduled tasks.</p>
|
|
133
|
-
<button class="task-create-btn" id="btn-create-task-empty"
|
|
154
|
+
<button class="task-create-btn" id="btn-create-task-empty">
|
|
155
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
156
|
+
<path d="M5 12h14"/>
|
|
157
|
+
<path d="M12 5v14"/>
|
|
158
|
+
</svg> Create Task
|
|
159
|
+
</button>`;
|
|
134
160
|
table.appendChild(empty);
|
|
135
161
|
const btn = table.querySelector("#btn-create-task-empty");
|
|
136
162
|
if (btn) btn.addEventListener("click", () => Tasks.createInSession());
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// theme.js — Theme switcher module
|
|
2
|
+
// Handles light/dark theme persistence and switching
|
|
3
|
+
|
|
4
|
+
const Theme = (() => {
|
|
5
|
+
const STORAGE_KEY = "clacky-theme";
|
|
6
|
+
const ATTR_NAME = "data-theme";
|
|
7
|
+
|
|
8
|
+
// Initialize theme from localStorage or system preference
|
|
9
|
+
function init() {
|
|
10
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
11
|
+
const theme = saved || (window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
|
|
12
|
+
apply(theme);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Apply theme to document
|
|
16
|
+
function apply(theme) {
|
|
17
|
+
document.documentElement.setAttribute(ATTR_NAME, theme);
|
|
18
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
19
|
+
|
|
20
|
+
// Update header toggle button if it exists
|
|
21
|
+
const headerToggle = document.getElementById("theme-toggle-header");
|
|
22
|
+
if (headerToggle) {
|
|
23
|
+
if (theme === "light") {
|
|
24
|
+
headerToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
25
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
|
|
26
|
+
</svg>`;
|
|
27
|
+
} else {
|
|
28
|
+
headerToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
29
|
+
<circle cx="12" cy="12" r="4"/>
|
|
30
|
+
<path d="M12 2v2"/>
|
|
31
|
+
<path d="M12 20v2"/>
|
|
32
|
+
<path d="m4.93 4.93 1.41 1.41"/>
|
|
33
|
+
<path d="m17.66 17.66 1.41 1.41"/>
|
|
34
|
+
<path d="M2 12h2"/>
|
|
35
|
+
<path d="M20 12h2"/>
|
|
36
|
+
<path d="m6.34 17.66-1.41 1.41"/>
|
|
37
|
+
<path d="m19.07 4.93-1.41 1.41"/>
|
|
38
|
+
</svg>`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Update settings toggle button if it exists (legacy)
|
|
43
|
+
const toggle = document.getElementById("theme-toggle");
|
|
44
|
+
if (toggle) {
|
|
45
|
+
const icon = theme === "light" ? "🌙" : "☀️";
|
|
46
|
+
const label = theme === "light" ? "Dark" : "Light";
|
|
47
|
+
toggle.innerHTML = `<span class="theme-icon">${icon}</span><span>${label}</span>`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Toggle between light and dark
|
|
52
|
+
function toggle() {
|
|
53
|
+
const current = document.documentElement.getAttribute(ATTR_NAME) || "dark";
|
|
54
|
+
const next = current === "dark" ? "light" : "dark";
|
|
55
|
+
apply(next);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get current theme
|
|
59
|
+
function current() {
|
|
60
|
+
return document.documentElement.getAttribute(ATTR_NAME) || "dark";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { init, toggle, current };
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
// Initialize theme on page load
|
|
67
|
+
Theme.init();
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
@@ -177,6 +177,20 @@ dependencies:
|
|
|
177
177
|
- - "~>"
|
|
178
178
|
- !ruby/object:Gem::Version
|
|
179
179
|
version: '2.1'
|
|
180
|
+
- !ruby/object:Gem::Dependency
|
|
181
|
+
name: rubyzip
|
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
|
183
|
+
requirements:
|
|
184
|
+
- - "~>"
|
|
185
|
+
- !ruby/object:Gem::Version
|
|
186
|
+
version: '3.0'
|
|
187
|
+
type: :runtime
|
|
188
|
+
prerelease: false
|
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
190
|
+
requirements:
|
|
191
|
+
- - "~>"
|
|
192
|
+
- !ruby/object:Gem::Version
|
|
193
|
+
version: '3.0'
|
|
180
194
|
description: OpenClacky is a Ruby CLI tool for interacting with AI models via OpenAI-compatible
|
|
181
195
|
APIs. It provides chat functionality and autonomous AI agent capabilities with tool
|
|
182
196
|
use.
|
|
@@ -336,6 +350,7 @@ files:
|
|
|
336
350
|
- lib/clacky/web/settings.js
|
|
337
351
|
- lib/clacky/web/skills.js
|
|
338
352
|
- lib/clacky/web/tasks.js
|
|
353
|
+
- lib/clacky/web/theme.js
|
|
339
354
|
- lib/clacky/web/ws.js
|
|
340
355
|
- scripts/install.sh
|
|
341
356
|
- scripts/uninstall.sh
|