openclacky 1.3.6 → 1.3.7
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 +30 -0
- data/lib/clacky/agent.rb +8 -1
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/ext.yml +18 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
- data/lib/clacky/default_extensions/general/ext.yml +18 -0
- data/lib/clacky/default_extensions/git/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
- data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
- data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
- data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
- data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
- data/lib/clacky/extension/api_loader.rb +136 -0
- data/lib/clacky/extension/cli_commands.rb +226 -0
- data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
- data/lib/clacky/extension/hook_loader.rb +77 -0
- data/lib/clacky/extension/loader.rb +483 -0
- data/lib/clacky/extension/packager.rb +226 -0
- data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
- data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
- data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
- data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
- data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
- data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
- data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
- data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
- data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
- data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
- data/lib/clacky/extension/scaffold.rb +55 -0
- data/lib/clacky/extension/verifier.rb +196 -0
- data/lib/clacky/identity.rb +70 -0
- data/lib/clacky/locales/en.rb +19 -1
- data/lib/clacky/locales/zh.rb +19 -1
- data/lib/clacky/platform_http_client.rb +18 -15
- data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
- data/lib/clacky/server/channel.rb +6 -0
- data/lib/clacky/server/http_server.rb +313 -144
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +748 -573
- data/lib/clacky/web/app.js +63 -15
- data/lib/clacky/web/auth.js +2 -0
- data/lib/clacky/web/components/notify.js +2 -0
- data/lib/clacky/web/components/sidebar.js +1 -5
- data/lib/clacky/web/core/ext.js +316 -79
- data/lib/clacky/web/features/backup/store.js +1 -0
- data/lib/clacky/web/features/backup/view.js +0 -1
- data/lib/clacky/web/features/brand/view.js +13 -5
- data/lib/clacky/web/features/extensions/store.js +184 -0
- data/lib/clacky/web/features/extensions/view.js +389 -0
- data/lib/clacky/web/features/new-session/store.js +186 -0
- data/lib/clacky/web/features/new-session/view.js +319 -0
- data/lib/clacky/web/features/share/view.js +63 -14
- data/lib/clacky/web/features/skills/store.js +3 -1
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +24 -6
- data/lib/clacky/web/features/trash/view.js +46 -9
- data/lib/clacky/web/features/workspace/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +0 -1
- data/lib/clacky/web/i18n.js +118 -106
- data/lib/clacky/web/index.html +138 -188
- data/lib/clacky/web/sessions.js +64 -269
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +2 -0
- data/lib/clacky/web/theme.js +2 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +55 -24
- data/benchmark/runner.rb +0 -441
- data/lib/clacky/api_extension_loader.rb +0 -168
- data/lib/clacky/default_agents/SOUL.md +0 -3
- data/lib/clacky/default_agents/USER.md +0 -1
- data/lib/clacky/default_agents/coding/profile.yml +0 -5
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_agents/general/profile.yml +0 -2
- data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
- data/lib/clacky/web/features/creator/store.js +0 -81
- data/lib/clacky/web/features/creator/view.js +0 -380
- /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -293,6 +293,25 @@ const Sessions = (() => {
|
|
|
293
293
|
|
|
294
294
|
let _userScrolledUp = false; // true if user manually scrolled away from bottom
|
|
295
295
|
|
|
296
|
+
let _agentsById = null;
|
|
297
|
+
let _agentsLoadingPromise = null;
|
|
298
|
+
|
|
299
|
+
function _ensureAgentsLoaded() {
|
|
300
|
+
if (_agentsById) return Promise.resolve(_agentsById);
|
|
301
|
+
if (_agentsLoadingPromise) return _agentsLoadingPromise;
|
|
302
|
+
_agentsLoadingPromise = fetch("/api/agents")
|
|
303
|
+
.then(r => (r.ok ? r.json() : { agents: [] }))
|
|
304
|
+
.then(data => {
|
|
305
|
+
const map = {};
|
|
306
|
+
((data && data.agents) || []).forEach(a => { map[a.id] = a; });
|
|
307
|
+
_agentsById = map;
|
|
308
|
+
return map;
|
|
309
|
+
})
|
|
310
|
+
.catch(() => (_agentsById = {}))
|
|
311
|
+
.finally(() => { _agentsLoadingPromise = null; });
|
|
312
|
+
return _agentsLoadingPromise;
|
|
313
|
+
}
|
|
314
|
+
|
|
296
315
|
function _isAtBottom(container) {
|
|
297
316
|
if (!container) return false;
|
|
298
317
|
const threshold = 150;
|
|
@@ -450,76 +469,9 @@ const Sessions = (() => {
|
|
|
450
469
|
// we call addEventListener directly (no ?. / no `if` guards). If any is
|
|
451
470
|
// missing, it means HTML and JS drifted and we want the loud error.
|
|
452
471
|
function _initNewSessionControls() {
|
|
453
|
-
// Split button: main (quick create)
|
|
454
472
|
document.getElementById("btn-new-session-inline")
|
|
455
|
-
.addEventListener("click", () =>
|
|
456
|
-
|
|
457
|
-
// Split button: arrow — show dropdown on hover, hide when leaving the whole wrap
|
|
458
|
-
const arrow = document.getElementById("btn-new-session-arrow");
|
|
459
|
-
const wrap = arrow.closest(".btn-split-wrap");
|
|
460
|
-
const dd = document.getElementById("new-session-dropdown");
|
|
461
|
-
|
|
462
|
-
arrow.addEventListener("mouseenter", () => { dd.hidden = false; });
|
|
463
|
-
dd.addEventListener("mouseenter", () => { dd.hidden = false; });
|
|
464
|
-
wrap.addEventListener("mouseleave", () => { dd.hidden = true; });
|
|
465
|
-
dd.addEventListener("mouseleave", () => { dd.hidden = true; });
|
|
473
|
+
.addEventListener("click", () => { location.hash = "#new"; });
|
|
466
474
|
|
|
467
|
-
// Dropdown item "Advanced Options…"
|
|
468
|
-
document.addEventListener("click", (e) => {
|
|
469
|
-
if (e.target && e.target.id === "btn-new-session-modal") {
|
|
470
|
-
dd.hidden = true;
|
|
471
|
-
Sessions.openNewSessionModal();
|
|
472
|
-
} else if (!wrap.contains(e.target)) {
|
|
473
|
-
dd.hidden = true;
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
// Welcome screen "+ New Session" button
|
|
478
|
-
document.getElementById("btn-welcome-new")
|
|
479
|
-
.addEventListener("click", () => Sessions.create("general"));
|
|
480
|
-
|
|
481
|
-
// Welcome screen starter chips: create a session, then prefill the prompt
|
|
482
|
-
document.querySelectorAll(".chip[data-welcome-prompt]").forEach((chip) => {
|
|
483
|
-
chip.addEventListener("click", async () => {
|
|
484
|
-
await Sessions.create("general");
|
|
485
|
-
const prompt = I18n.t(chip.dataset.welcomePrompt);
|
|
486
|
-
const input = document.getElementById("user-input");
|
|
487
|
-
if (input && prompt) {
|
|
488
|
-
input.value = prompt;
|
|
489
|
-
input.focus();
|
|
490
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// Modal: cancel / create / overlay click
|
|
496
|
-
document.getElementById("new-session-cancel")
|
|
497
|
-
.addEventListener("click", () => Sessions.closeNewSessionModal());
|
|
498
|
-
document.getElementById("new-session-create")
|
|
499
|
-
.addEventListener("click", () => Sessions.createFromModal());
|
|
500
|
-
document.getElementById("new-session-modal")
|
|
501
|
-
.addEventListener("click", (e) => {
|
|
502
|
-
// Only close when the click lands on the overlay itself, not on
|
|
503
|
-
// the inner dialog panel.
|
|
504
|
-
if (e.target.id === "new-session-modal") {
|
|
505
|
-
Sessions.closeNewSessionModal();
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// Working-directory browse button → reuses the session picker in
|
|
510
|
-
// session-less mode (no session exists yet, so it browses via /api/dirs).
|
|
511
|
-
const browseBtn = document.getElementById("new-session-browse-btn");
|
|
512
|
-
if (browseBtn) {
|
|
513
|
-
browseBtn.addEventListener("click", async () => {
|
|
514
|
-
const dirInput = document.getElementById("new-session-directory");
|
|
515
|
-
const start = dirInput ? dirInput.value.trim() : "";
|
|
516
|
-
const picked = await window.openDirectoryPicker(start, null);
|
|
517
|
-
if (picked && dirInput) dirInput.value = picked;
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Load-more sessions button is rendered dynamically by renderList(),
|
|
522
|
-
// so we listen via event delegation.
|
|
523
475
|
document.addEventListener("click", (e) => {
|
|
524
476
|
if (e.target && e.target.id === "btn-load-more-sessions") {
|
|
525
477
|
Sessions.loadMore();
|
|
@@ -639,11 +591,11 @@ const Sessions = (() => {
|
|
|
639
591
|
|
|
640
592
|
function _addImageFile(file) {
|
|
641
593
|
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
|
642
|
-
alert(
|
|
594
|
+
alert(I18n.t("chat.file.unsupportedType", { type: file.type }));
|
|
643
595
|
return;
|
|
644
596
|
}
|
|
645
597
|
if (file.size > MAX_IMAGE_SIZE) {
|
|
646
|
-
alert(
|
|
598
|
+
alert(I18n.t("chat.file.imageTooLarge", { name: file.name, max: "5 MB" }));
|
|
647
599
|
return;
|
|
648
600
|
}
|
|
649
601
|
const seq = ++_imageSeq;
|
|
@@ -655,12 +607,12 @@ const Sessions = (() => {
|
|
|
655
607
|
_pendingImages.push({ dataUrl, name: displayName, mimeType: file.type === "image/png" ? "image/png" : "image/jpeg", seq });
|
|
656
608
|
_renderAttachmentPreviews();
|
|
657
609
|
})
|
|
658
|
-
.catch(err => alert(
|
|
610
|
+
.catch(err => alert(I18n.t("chat.file.processFailed", { msg: err.message })));
|
|
659
611
|
}
|
|
660
612
|
|
|
661
613
|
function _addGenericFile(file) {
|
|
662
614
|
if (file.size > MAX_FILE_BYTES) {
|
|
663
|
-
alert(
|
|
615
|
+
alert(I18n.t("chat.file.tooLarge", { name: file.name, max: "32 MB" }));
|
|
664
616
|
return;
|
|
665
617
|
}
|
|
666
618
|
// Upload file to server via HTTP — only the path is returned, no base64 in memory
|
|
@@ -669,7 +621,7 @@ const Sessions = (() => {
|
|
|
669
621
|
fetch("/api/upload", { method: "POST", body: formData })
|
|
670
622
|
.then(r => r.json())
|
|
671
623
|
.then(data => {
|
|
672
|
-
if (!data.ok) { alert(
|
|
624
|
+
if (!data.ok) { alert(I18n.t("chat.file.uploadFailed", { msg: data.error })); return; }
|
|
673
625
|
_pendingFiles.push({
|
|
674
626
|
name: data.name,
|
|
675
627
|
path: data.path,
|
|
@@ -2166,11 +2118,22 @@ const Sessions = (() => {
|
|
|
2166
2118
|
? `<span class="session-badge session-badge--${s.source}">${I18n.t(badgeKey)}</span>`
|
|
2167
2119
|
: "";
|
|
2168
2120
|
|
|
2169
|
-
//
|
|
2170
|
-
//
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2121
|
+
// Agent profile badge: any non-general profile gets a badge. Coding
|
|
2122
|
+
// (built-in) keeps its i18n label + hollow neutral style; extension /
|
|
2123
|
+
// custom agents fall back to their configured title (or profile id).
|
|
2124
|
+
let agentBadgeHtml = "";
|
|
2125
|
+
const profile = s.agent_profile;
|
|
2126
|
+
if (profile && profile !== "general") {
|
|
2127
|
+
if (profile === "coding") {
|
|
2128
|
+
agentBadgeHtml = `<span class="session-badge session-badge--coding">${I18n.t("sessions.badge.coding")}</span>`;
|
|
2129
|
+
} else {
|
|
2130
|
+
const meta = _agentsById && _agentsById[profile];
|
|
2131
|
+
const isZh = I18n.lang && I18n.lang().startsWith("zh");
|
|
2132
|
+
const label = (meta && ((isZh && meta.title_zh) || meta.title)) || profile;
|
|
2133
|
+
const safeLabel = escapeHtml(label);
|
|
2134
|
+
agentBadgeHtml = `<span class="session-badge session-badge--agent" title="${safeLabel}">${safeLabel}</span>`;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2174
2137
|
|
|
2175
2138
|
// Pin icon (always visible for pinned sessions)
|
|
2176
2139
|
const pinIcon = s.pinned ? `<span class="session-pin-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(45deg);display:block"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg></span>` : "";
|
|
@@ -2187,7 +2150,7 @@ const Sessions = (() => {
|
|
|
2187
2150
|
|
|
2188
2151
|
el.innerHTML = `
|
|
2189
2152
|
<div class="session-body">
|
|
2190
|
-
<div class="session-name">${dotHtml}<span class="session-name__text">${nameHtml}</span>${badgeHtml}${
|
|
2153
|
+
<div class="session-name">${dotHtml}<span class="session-name__text">${nameHtml}</span>${badgeHtml}${agentBadgeHtml}${pinIcon}</div>
|
|
2191
2154
|
<div class="session-meta">${metaText}</div>
|
|
2192
2155
|
${snippetHtml}
|
|
2193
2156
|
</div>
|
|
@@ -2325,6 +2288,7 @@ const Sessions = (() => {
|
|
|
2325
2288
|
_initComposer();
|
|
2326
2289
|
_initSearch();
|
|
2327
2290
|
_initMessageHistory();
|
|
2291
|
+
_ensureAgentsLoaded().then(() => Sessions.renderList());
|
|
2328
2292
|
// Re-render session list (badges/labels) when the user switches language
|
|
2329
2293
|
document.addEventListener("langchange", () => Sessions.renderList());
|
|
2330
2294
|
|
|
@@ -2899,7 +2863,7 @@ const Sessions = (() => {
|
|
|
2899
2863
|
let cronEntryRendered = false;
|
|
2900
2864
|
|
|
2901
2865
|
nonCron.forEach(s => {
|
|
2902
|
-
if (!cronEntryRendered && _latestCronUpdatedAt) {
|
|
2866
|
+
if (!cronEntryRendered && !s.pinned && _latestCronUpdatedAt) {
|
|
2903
2867
|
const t = s.updated_at || s.created_at;
|
|
2904
2868
|
if (!t || t < _latestCronUpdatedAt) {
|
|
2905
2869
|
_renderCronGroupItem(list, _cronCount, cronHasRunning);
|
|
@@ -2937,14 +2901,22 @@ const Sessions = (() => {
|
|
|
2937
2901
|
// has scrolled away to browse other sessions.
|
|
2938
2902
|
if (scrollToActive) {
|
|
2939
2903
|
const activeEl = list.querySelector(".session-item.active");
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
//
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2904
|
+
const sidebarList = document.getElementById("sidebar-list");
|
|
2905
|
+
if (activeEl && sidebarList) {
|
|
2906
|
+
// Only scroll when the active row is not already fully visible.
|
|
2907
|
+
// The "Sessions" divider is position:sticky at the top of
|
|
2908
|
+
// #sidebar-list, so the usable viewport starts *below* it — treat
|
|
2909
|
+
// that as the top edge, otherwise a row tucked under the header
|
|
2910
|
+
// reads as "visible" and never gets revealed.
|
|
2911
|
+
const header = sidebarList.querySelector(".sidebar-divider");
|
|
2912
|
+
const headerH = (header && header.offsetParent) ? header.offsetHeight : 0;
|
|
2913
|
+
const listRect = sidebarList.getBoundingClientRect();
|
|
2914
|
+
const rowRect = activeEl.getBoundingClientRect();
|
|
2915
|
+
const topEdge = listRect.top + headerH;
|
|
2916
|
+
if (rowRect.top < topEdge) {
|
|
2917
|
+
sidebarList.scrollTop += rowRect.top - topEdge;
|
|
2918
|
+
} else if (rowRect.bottom > listRect.bottom) {
|
|
2919
|
+
sidebarList.scrollTop += rowRect.bottom - listRect.bottom;
|
|
2948
2920
|
}
|
|
2949
2921
|
}
|
|
2950
2922
|
}
|
|
@@ -3189,7 +3161,7 @@ const Sessions = (() => {
|
|
|
3189
3161
|
/** Update the session info bar below the chat header with current session metadata. */
|
|
3190
3162
|
updateInfoBar(s) {
|
|
3191
3163
|
this._lastSession = s;
|
|
3192
|
-
if (
|
|
3164
|
+
if (Clacky.Workspace) Clacky.Workspace.onSession(s);
|
|
3193
3165
|
if (!s) {
|
|
3194
3166
|
// Hide all spans when no session
|
|
3195
3167
|
["sib-id", "sib-status", "sib-dir", "sib-mode", "sib-model", "sib-reasoning", "sib-tasks", "sib-cost"].forEach(id => {
|
|
@@ -4028,187 +4000,8 @@ const Sessions = (() => {
|
|
|
4028
4000
|
_pendingMessage = null;
|
|
4029
4001
|
return msg;
|
|
4030
4002
|
},
|
|
4031
|
-
|
|
4032
|
-
// ── New Session Modal ──────────────────────────────────────────────────
|
|
4033
|
-
|
|
4034
|
-
/** Open the New Session modal with configuration options. */
|
|
4035
|
-
openNewSessionModal() {
|
|
4036
|
-
const modal = $("new-session-modal");
|
|
4037
|
-
if (!modal) return;
|
|
4038
|
-
|
|
4039
|
-
// Populate model dropdown from configured models
|
|
4040
|
-
_populateModelDropdown();
|
|
4041
|
-
|
|
4042
|
-
// Set default working directory to an absolute path (home/clacky_workspace).
|
|
4043
|
-
const dirInput = $("new-session-directory");
|
|
4044
|
-
if (dirInput && !dirInput.value) {
|
|
4045
|
-
fetch("/api/dirs")
|
|
4046
|
-
.then(r => r.ok ? r.json() : null)
|
|
4047
|
-
.then(data => {
|
|
4048
|
-
const home = data && data.home;
|
|
4049
|
-
if (home && !dirInput.value) {
|
|
4050
|
-
dirInput.value = home.replace(/\/+$/, "") + "/clacky_workspace";
|
|
4051
|
-
}
|
|
4052
|
-
})
|
|
4053
|
-
.catch(() => {});
|
|
4054
|
-
}
|
|
4055
|
-
|
|
4056
|
-
// Setup agent type change listener to show/hide init project checkbox
|
|
4057
|
-
const agentSelect = $("new-session-agent");
|
|
4058
|
-
const initProjectField = $("new-session-init-project-field");
|
|
4059
|
-
|
|
4060
|
-
if (agentSelect && initProjectField) {
|
|
4061
|
-
// Set initial state based on current selection
|
|
4062
|
-
initProjectField.style.display = agentSelect.value === "coding" ? "block" : "none";
|
|
4063
|
-
|
|
4064
|
-
// Listen for changes
|
|
4065
|
-
agentSelect.addEventListener("change", function() {
|
|
4066
|
-
initProjectField.style.display = this.value === "coding" ? "block" : "none";
|
|
4067
|
-
});
|
|
4068
|
-
}
|
|
4069
|
-
|
|
4070
|
-
// Show modal
|
|
4071
|
-
modal.style.display = "flex";
|
|
4072
|
-
},
|
|
4073
|
-
|
|
4074
|
-
/** Close the New Session modal. */
|
|
4075
|
-
closeNewSessionModal() {
|
|
4076
|
-
const modal = $("new-session-modal");
|
|
4077
|
-
if (modal) modal.style.display = "none";
|
|
4078
|
-
},
|
|
4079
|
-
|
|
4080
|
-
/** Create session from modal form data. */
|
|
4081
|
-
async createFromModal() {
|
|
4082
|
-
const agentSelect = $("new-session-agent");
|
|
4083
|
-
const nameInput = $("new-session-name");
|
|
4084
|
-
const modelSelect = $("new-session-model");
|
|
4085
|
-
const dirInput = $("new-session-directory");
|
|
4086
|
-
const initCheckbox = $("new-session-init-project");
|
|
4087
|
-
const createBtn = $("new-session-create");
|
|
4088
|
-
|
|
4089
|
-
const agentProfile = agentSelect ? agentSelect.value : "general";
|
|
4090
|
-
const customName = nameInput ? nameInput.value.trim() : "";
|
|
4091
|
-
// The dropdown's value is the model's stable runtime id (see
|
|
4092
|
-
// _populateModelDropdown). Using the id — not the model *name* — lets
|
|
4093
|
-
// the backend switch to the right full model entry (api_key, base_url,
|
|
4094
|
-
// anthropic_format) instead of mutating the current default entry's
|
|
4095
|
-
// name in place, which caused "unknown model <name>" errors when the
|
|
4096
|
-
// chosen model belonged to a different provider than the default.
|
|
4097
|
-
const selectedModelId = modelSelect ? modelSelect.value : "";
|
|
4098
|
-
const workingDir = dirInput ? dirInput.value.trim() : "";
|
|
4099
|
-
const initProject = initCheckbox ? initCheckbox.checked : false;
|
|
4100
|
-
|
|
4101
|
-
// Auto-generate name if not provided
|
|
4102
|
-
let name = customName;
|
|
4103
|
-
if (!name) {
|
|
4104
|
-
const maxN = _sessions.reduce((max, s) => {
|
|
4105
|
-
const m = s.name.match(/^Session (\d+)$/);
|
|
4106
|
-
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
4107
|
-
}, 0);
|
|
4108
|
-
name = "Session " + (maxN + 1);
|
|
4109
|
-
}
|
|
4110
|
-
|
|
4111
|
-
if (createBtn) createBtn.disabled = true;
|
|
4112
|
-
|
|
4113
|
-
try {
|
|
4114
|
-
const payload = {
|
|
4115
|
-
name,
|
|
4116
|
-
agent_profile: agentProfile,
|
|
4117
|
-
source: "manual"
|
|
4118
|
-
};
|
|
4119
|
-
|
|
4120
|
-
// Add optional fields
|
|
4121
|
-
if (workingDir) payload.working_dir = workingDir;
|
|
4122
|
-
if (selectedModelId) payload.model_id = selectedModelId;
|
|
4123
|
-
|
|
4124
|
-
const res = await fetch("/api/sessions", {
|
|
4125
|
-
method: "POST",
|
|
4126
|
-
headers: { "Content-Type": "application/json" },
|
|
4127
|
-
body: JSON.stringify(payload)
|
|
4128
|
-
});
|
|
4129
|
-
const data = await res.json();
|
|
4130
|
-
|
|
4131
|
-
if (!res.ok) {
|
|
4132
|
-
const msg = data.error || "unknown error";
|
|
4133
|
-
const friendly = res.status === 409
|
|
4134
|
-
? I18n.t("sessions.dirNotEmpty")
|
|
4135
|
-
: I18n.t("sessions.createError") + msg;
|
|
4136
|
-
alert(friendly);
|
|
4137
|
-
if (createBtn) createBtn.disabled = false;
|
|
4138
|
-
return;
|
|
4139
|
-
}
|
|
4140
|
-
|
|
4141
|
-
const session = data.session;
|
|
4142
|
-
if (!session) return;
|
|
4143
|
-
|
|
4144
|
-
// Close modal and reset form
|
|
4145
|
-
Sessions.closeNewSessionModal();
|
|
4146
|
-
if (nameInput) nameInput.value = "";
|
|
4147
|
-
if (dirInput) dirInput.value = "";
|
|
4148
|
-
if (initCheckbox) initCheckbox.checked = false;
|
|
4149
|
-
|
|
4150
|
-
// Add to list and select
|
|
4151
|
-
Sessions.add(session);
|
|
4152
|
-
Sessions.renderList();
|
|
4153
|
-
Sessions.select(session.id);
|
|
4154
|
-
|
|
4155
|
-
// If init project was checked, send /new command
|
|
4156
|
-
if (initProject) {
|
|
4157
|
-
Sessions.setPendingMessage(session.id, "/new");
|
|
4158
|
-
}
|
|
4159
|
-
} catch (e) {
|
|
4160
|
-
alert(I18n.t("sessions.createError") + e.message);
|
|
4161
|
-
} finally {
|
|
4162
|
-
if (createBtn) createBtn.disabled = false;
|
|
4163
|
-
}
|
|
4164
|
-
},
|
|
4165
4003
|
};
|
|
4166
4004
|
|
|
4167
|
-
// ── Helper: Populate model dropdown ────────────────────────────────────────
|
|
4168
|
-
|
|
4169
|
-
async function _populateModelDropdown() {
|
|
4170
|
-
const modelSelect = $("new-session-model");
|
|
4171
|
-
if (!modelSelect) return;
|
|
4172
|
-
|
|
4173
|
-
try {
|
|
4174
|
-
const res = await fetch("/api/config");
|
|
4175
|
-
const data = await res.json();
|
|
4176
|
-
const models = data.models || [];
|
|
4177
|
-
|
|
4178
|
-
modelSelect.innerHTML = "";
|
|
4179
|
-
|
|
4180
|
-
if (models.length === 0) {
|
|
4181
|
-
const opt = document.createElement("option");
|
|
4182
|
-
opt.value = "";
|
|
4183
|
-
opt.textContent = "No models configured";
|
|
4184
|
-
modelSelect.appendChild(opt);
|
|
4185
|
-
return;
|
|
4186
|
-
}
|
|
4187
|
-
|
|
4188
|
-
// Add each configured model (CLI-style format).
|
|
4189
|
-
// The option's value is the model's stable runtime id — not the bare
|
|
4190
|
-
// model name — so the backend can switch to the exact model entry
|
|
4191
|
-
// (with matching api_key / base_url / anthropic_format) when the user
|
|
4192
|
-
// chooses a non-default model. See createFromModal + build_session.
|
|
4193
|
-
models.forEach(m => {
|
|
4194
|
-
const opt = document.createElement("option");
|
|
4195
|
-
opt.value = m.id || "";
|
|
4196
|
-
|
|
4197
|
-
// Format: [default] abs-claude-sonnet-4-5 (clacky...8825)
|
|
4198
|
-
const typeBadge = m.type === "default" ? "[default] " : "";
|
|
4199
|
-
const label = `${typeBadge}${m.model} (${m.api_key_masked})`;
|
|
4200
|
-
opt.textContent = label;
|
|
4201
|
-
|
|
4202
|
-
// Pre-select default model
|
|
4203
|
-
if (m.type === "default") opt.selected = true;
|
|
4204
|
-
modelSelect.appendChild(opt);
|
|
4205
|
-
});
|
|
4206
|
-
} catch (e) {
|
|
4207
|
-
console.error("Failed to load models:", e);
|
|
4208
|
-
modelSelect.innerHTML = '<option value="">Error loading models</option>';
|
|
4209
|
-
}
|
|
4210
|
-
}
|
|
4211
|
-
|
|
4212
4005
|
return Sessions;
|
|
4213
4006
|
})();
|
|
4214
4007
|
|
|
@@ -5681,7 +5474,7 @@ const Sessions = (() => {
|
|
|
5681
5474
|
// ── Session Info Bar Reasoning Effort Switcher ────────────────────────────
|
|
5682
5475
|
(function() {
|
|
5683
5476
|
let _isOpen = false;
|
|
5684
|
-
const LEVELS = ["off", "low", "medium", "high"];
|
|
5477
|
+
const LEVELS = ["off", "low", "medium", "high", "xhigh"];
|
|
5685
5478
|
|
|
5686
5479
|
document.addEventListener("click", async (e) => {
|
|
5687
5480
|
const el = e.target.closest("#sib-reasoning");
|
|
@@ -5790,3 +5583,5 @@ document.addEventListener("currencychange", () => {
|
|
|
5790
5583
|
scrollTimer = setTimeout(() => sidebarList.classList.remove("is-scrolling"), 1000);
|
|
5791
5584
|
}, { passive: true });
|
|
5792
5585
|
})();
|
|
5586
|
+
|
|
5587
|
+
Clacky.Sessions = Sessions;
|
data/lib/clacky/web/settings.js
CHANGED
data/lib/clacky/web/skills.js
CHANGED
data/lib/clacky/web/theme.js
CHANGED
|
@@ -30,11 +30,13 @@ const Theme = (() => {
|
|
|
30
30
|
// Icon shows what you'd switch TO, not what you are on.
|
|
31
31
|
if (theme === "light") {
|
|
32
32
|
// In light mode → show moon (click to go dark)
|
|
33
|
+
headerToggle.title = I18n.t("header.theme.toDark");
|
|
33
34
|
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">
|
|
34
35
|
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
|
|
35
36
|
</svg>`;
|
|
36
37
|
} else {
|
|
37
38
|
// In dark mode → show sun (click to go light)
|
|
39
|
+
headerToggle.title = I18n.t("header.theme.toLight");
|
|
38
40
|
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">
|
|
39
41
|
<circle cx="12" cy="12" r="4"/>
|
|
40
42
|
<path d="M12 2v2"/>
|
|
@@ -281,6 +281,7 @@ WS.onEvent(ev => {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
case "session_deleted":
|
|
284
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.notifySessionRemoved(ev.session_id);
|
|
284
285
|
Sessions.remove(ev.session_id);
|
|
285
286
|
if (ev.session_id === Sessions.activeId) Router.navigate("welcome");
|
|
286
287
|
Sessions.renderList();
|
data/lib/clacky/web/ws.js
CHANGED
data/lib/clacky.rb
CHANGED
|
@@ -95,6 +95,7 @@ require_relative "clacky/ui2/progress_indicator"
|
|
|
95
95
|
require_relative "clacky/utils/logger"
|
|
96
96
|
require_relative "clacky/proxy_config"
|
|
97
97
|
require_relative "clacky/platform_http_client"
|
|
98
|
+
require_relative "clacky/identity"
|
|
98
99
|
require_relative "clacky/utils/encoding"
|
|
99
100
|
require_relative "clacky/utils/environment_detector"
|
|
100
101
|
require_relative "clacky/utils/browser_detector"
|
|
@@ -140,18 +141,28 @@ require_relative "clacky/server/browser_manager"
|
|
|
140
141
|
require_relative "clacky/server/backup_manager"
|
|
141
142
|
require_relative "clacky/cli"
|
|
142
143
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
require_relative "clacky/
|
|
144
|
+
# Extension architecture: define the base class + container loader + scaffolding
|
|
145
|
+
# + dispatcher up front so the patch / hook loaders below can read its result.
|
|
146
|
+
require_relative "clacky/extension/api_extension"
|
|
147
|
+
require_relative "clacky/extension/api_loader"
|
|
148
|
+
require_relative "clacky/extension/loader"
|
|
149
|
+
require_relative "clacky/extension/scaffold/template_renderer"
|
|
150
|
+
require_relative "clacky/extension/scaffold"
|
|
151
|
+
require_relative "clacky/extension/packager"
|
|
152
|
+
require_relative "clacky/extension/dispatcher"
|
|
153
|
+
require_relative "clacky/extension/hook_loader"
|
|
154
|
+
require_relative "clacky/extension/verifier"
|
|
155
|
+
Clacky::ExtensionLoader.load_all
|
|
156
|
+
|
|
157
|
+
# Runtime patch layer: load user/AI patches from ~/.clacky/patches/ AND patches
|
|
158
|
+
# contributed by ext.yml containers (contributes.patches). Runs after all gem
|
|
159
|
+
# code is defined, so fingerprints reflect the actual installed source.
|
|
160
|
+
require_relative "clacky/extension/patch_loader"
|
|
146
161
|
Clacky::PatchLoader.load_all
|
|
147
162
|
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
# like session_manager or agent_config.
|
|
152
|
-
require_relative "clacky/api_extension"
|
|
153
|
-
require_relative "clacky/api_extension_loader"
|
|
154
|
-
require_relative "clacky/server/api_extension_dispatcher"
|
|
163
|
+
# Process-wide registry of hook callbacks contributed by ext.yml containers
|
|
164
|
+
# (contributes.hooks). Each agent copies them onto its own HookManager at init.
|
|
165
|
+
Clacky::ExtensionHookLoader.load_all
|
|
155
166
|
|
|
156
167
|
module Clacky
|
|
157
168
|
class AgentInterrupted < Exception; end # Inherit from Exception to bypass rescue StandardError
|