openclacky 1.4.0 → 1.4.1
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 +29 -0
- data/lib/clacky/agent/session_serializer.rb +11 -4
- data/lib/clacky/agent.rb +4 -5
- data/lib/clacky/brand_config.rb +53 -3
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +144 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +412 -173
- data/lib/clacky/extension/packager.rb +10 -1
- data/lib/clacky/platform_http_client.rb +9 -7
- data/lib/clacky/providers.rb +4 -4
- data/lib/clacky/server/http_server.rb +19 -18
- data/lib/clacky/tools/grep.rb +6 -1
- data/lib/clacky/utils/environment_detector.rb +16 -9
- data/lib/clacky/utils/file_processor.rb +21 -24
- data/lib/clacky/utils/model_pricing.rb +49 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +121 -17
- data/lib/clacky/web/components/onboard.js +14 -4
- data/lib/clacky/web/features/backup/view.js +4 -3
- data/lib/clacky/web/features/extensions/store.js +2 -2
- data/lib/clacky/web/features/extensions/view.js +14 -19
- data/lib/clacky/web/features/mcp/view.js +5 -2
- data/lib/clacky/web/features/workspace/store.js +11 -0
- data/lib/clacky/web/features/workspace/view.js +10 -4
- data/lib/clacky/web/i18n.js +6 -18
- data/lib/clacky/web/index.html +23 -2
- data/lib/clacky/web/sessions.js +33 -5
- data/scripts/uninstall.sh +1 -1
- metadata +1 -1
|
@@ -22,7 +22,7 @@ const ExtensionsStore = (() => {
|
|
|
22
22
|
let _extensions = []; // [{ id, name, name_zh, description, ..., units }]
|
|
23
23
|
let _allExtensions = []; // unfiltered result from server
|
|
24
24
|
let _query = ""; // current search text
|
|
25
|
-
let _sort = "
|
|
25
|
+
let _sort = "downloads"; // "newest" | "updated" | "downloads"
|
|
26
26
|
let _filterInstalled = false; // when true, show only installed extensions
|
|
27
27
|
let _loading = false;
|
|
28
28
|
let _error = null; // soft warning when the store is unreachable
|
|
@@ -102,7 +102,7 @@ const ExtensionsStore = (() => {
|
|
|
102
102
|
|
|
103
103
|
/** Set the sort order and reload. */
|
|
104
104
|
setSort(sort) {
|
|
105
|
-
_sort = sort || "
|
|
105
|
+
_sort = sort || "downloads";
|
|
106
106
|
return Extensions.load();
|
|
107
107
|
},
|
|
108
108
|
|
|
@@ -291,10 +291,23 @@ const ExtensionsView = (() => {
|
|
|
291
291
|
${_renderActions(ext)}
|
|
292
292
|
</div>
|
|
293
293
|
</div>
|
|
294
|
-
${
|
|
294
|
+
${_renderReadme(ext.readme)}
|
|
295
295
|
${_renderVersions(ext.versions)}`;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// Renders Markdown readme content. Uses marked.js if available, falls back to plain text.
|
|
299
|
+
function _renderReadme(readme) {
|
|
300
|
+
if (!readme || !readme.trim()) return "";
|
|
301
|
+
const html = typeof marked !== "undefined"
|
|
302
|
+
? marked.parse(readme, { breaks: true, gfm: true })
|
|
303
|
+
: `<pre style="white-space:pre-wrap">${escapeHtml(readme)}</pre>`;
|
|
304
|
+
return `
|
|
305
|
+
<div class="extension-detail-block extension-readme">
|
|
306
|
+
<h3 class="extension-detail-block-title">${escapeHtml(I18n.t("extensions.detail.readme"))}</h3>
|
|
307
|
+
<div class="extension-readme-body">${html}</div>
|
|
308
|
+
</div>`;
|
|
309
|
+
}
|
|
310
|
+
|
|
298
311
|
// Manage buttons for a locally installed extension: enable/disable toggle
|
|
299
312
|
// (always available when installed) plus remove (installed layer only).
|
|
300
313
|
function _renderActions(ext) {
|
|
@@ -374,24 +387,6 @@ const ExtensionsView = (() => {
|
|
|
374
387
|
</div>`;
|
|
375
388
|
}
|
|
376
389
|
|
|
377
|
-
function _renderEntryPoints(contributes) {
|
|
378
|
-
if (!contributes || !Array.isArray(contributes.panels)) return "";
|
|
379
|
-
const eps = contributes.panels.flatMap((p) => Array.isArray(p.entry_points) ? p.entry_points : []);
|
|
380
|
-
if (eps.length === 0) return "";
|
|
381
|
-
const rows = eps.map((ep) => {
|
|
382
|
-
const slotKey = "extensions.slot." + ep.slot;
|
|
383
|
-
const label = I18n.t(slotKey) !== slotKey ? I18n.t(slotKey) : ep.slot;
|
|
384
|
-
return `<li class="extension-contrib-item"><span class="extension-contrib-title">${escapeHtml(label)}</span></li>`;
|
|
385
|
-
}).join("");
|
|
386
|
-
return `
|
|
387
|
-
<div class="extension-detail-block">
|
|
388
|
-
<h3 class="extension-detail-block-title">${escapeHtml(I18n.t("extensions.detail.entry_points"))}</h3>
|
|
389
|
-
<div class="extension-detail-section">
|
|
390
|
-
<ul class="extension-contrib-list">${rows}</ul>
|
|
391
|
-
</div>
|
|
392
|
-
</div>`;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
390
|
function _renderVersions(versions) {
|
|
396
391
|
if (!Array.isArray(versions) || versions.length === 0) return "";
|
|
397
392
|
const rows = versions.map((v) => {
|
|
@@ -84,6 +84,11 @@ const McpView = (() => {
|
|
|
84
84
|
|
|
85
85
|
data.servers.forEach(server => {
|
|
86
86
|
list.appendChild(_renderCard(server));
|
|
87
|
+
// _renderTools uses getElementById which requires the card to already be
|
|
88
|
+
// in the DOM, so we call it after appendChild (not inside _renderCard).
|
|
89
|
+
if (McpStore.state.isExpanded(server.name) && !server.disabled) {
|
|
90
|
+
_renderTools(server.name);
|
|
91
|
+
}
|
|
87
92
|
});
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -170,8 +175,6 @@ const McpView = (() => {
|
|
|
170
175
|
card.querySelector(`#toggle-mcp-${CSS.escape(server.name)}`)
|
|
171
176
|
?.addEventListener("change", (ev) => _toggle(server.name, ev.target.checked));
|
|
172
177
|
|
|
173
|
-
if (isExpanded && !server.disabled) _renderTools(server.name);
|
|
174
|
-
|
|
175
178
|
return card;
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -72,6 +72,17 @@ const WorkspaceStore = (() => {
|
|
|
72
72
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
73
73
|
},
|
|
74
74
|
|
|
75
|
+
async displayPath(entry) {
|
|
76
|
+
const resp = await fetch("/api/file-action", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ path: _absPath(entry.path), action: "display-path" })
|
|
80
|
+
});
|
|
81
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
82
|
+
const data = await resp.json();
|
|
83
|
+
return data.path;
|
|
84
|
+
},
|
|
85
|
+
|
|
75
86
|
async fetchFileBlob(entry) {
|
|
76
87
|
const resp = await fetch("/api/file-action", {
|
|
77
88
|
method: "POST",
|
|
@@ -156,10 +156,16 @@ const WorkspaceView = (() => {
|
|
|
156
156
|
if (existing) existing.remove();
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
function copyPath(entry) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
async function copyPath(entry) {
|
|
160
|
+
const fallback = Workspace.state.workingDir.replace(/\/+$/, "") + "/" + entry.path.replace(/^\/+/, "");
|
|
161
|
+
let target = fallback;
|
|
162
|
+
try {
|
|
163
|
+
target = await Workspace.displayPath(entry);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
target = fallback;
|
|
166
|
+
}
|
|
167
|
+
navigator.clipboard.writeText(target).then(() => {
|
|
168
|
+
Modal.toast(target, "info");
|
|
163
169
|
});
|
|
164
170
|
}
|
|
165
171
|
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -179,7 +179,8 @@ const I18n = (() => {
|
|
|
179
179
|
"sessions.badge.channel": "Channel",
|
|
180
180
|
"sessions.badge.coding": "Coding",
|
|
181
181
|
"sessions.badge.setup": "Setup",
|
|
182
|
-
"sessions.newSession": "
|
|
182
|
+
"sessions.newSession": "New Session",
|
|
183
|
+
"sessions.advancedOptions": "Advanced options",
|
|
183
184
|
"sessions.actions.pin": "Pin",
|
|
184
185
|
"sessions.actions.unpin": "Unpin",
|
|
185
186
|
"sessions.actions.fork": "Fork",
|
|
@@ -537,16 +538,9 @@ const I18n = (() => {
|
|
|
537
538
|
"extensions.unit.api": "API",
|
|
538
539
|
"extensions.unit.apis": "APIs",
|
|
539
540
|
"extensions.detail.back": "Back",
|
|
541
|
+
"extensions.detail.readme": "Description",
|
|
540
542
|
"extensions.detail.contributes": "What's inside",
|
|
541
|
-
"extensions.detail.entry_points": "Access points",
|
|
542
543
|
"extensions.detail.versions": "Version history",
|
|
543
|
-
"extensions.slot.sidebar.nav": "Sidebar nav",
|
|
544
|
-
"extensions.slot.sidebar.nav.top": "Sidebar nav (top)",
|
|
545
|
-
"extensions.slot.sidebar.nav.bottom": "Sidebar nav (bottom)",
|
|
546
|
-
"extensions.slot.sidebar.footer": "Sidebar footer",
|
|
547
|
-
"extensions.slot.main.workspace": "Main workspace",
|
|
548
|
-
"extensions.slot.session.aside": "Session aside",
|
|
549
|
-
"extensions.slot.settings.tabs": "Settings tab",
|
|
550
544
|
"extensions.section.agents": "Agents",
|
|
551
545
|
"extensions.section.skills": "Skills",
|
|
552
546
|
"extensions.section.panels": "Panels",
|
|
@@ -1159,7 +1153,8 @@ const I18n = (() => {
|
|
|
1159
1153
|
"sessions.badge.channel": "频道",
|
|
1160
1154
|
"sessions.badge.coding": "Coding",
|
|
1161
1155
|
"sessions.badge.setup": "配置",
|
|
1162
|
-
"sessions.newSession": "
|
|
1156
|
+
"sessions.newSession": "新会话",
|
|
1157
|
+
"sessions.advancedOptions": "高级选项",
|
|
1163
1158
|
"sessions.loadMore": "加载更多会话",
|
|
1164
1159
|
"sessions.actions.pin": "置顶",
|
|
1165
1160
|
"sessions.actions.unpin": "取消置顶",
|
|
@@ -1516,16 +1511,9 @@ const I18n = (() => {
|
|
|
1516
1511
|
"extensions.unit.api": "个 API",
|
|
1517
1512
|
"extensions.unit.apis": "个 API",
|
|
1518
1513
|
"extensions.detail.back": "返回",
|
|
1514
|
+
"extensions.detail.readme": "使用说明",
|
|
1519
1515
|
"extensions.detail.contributes": "包含内容",
|
|
1520
|
-
"extensions.detail.entry_points": "访问入口",
|
|
1521
1516
|
"extensions.detail.versions": "版本历史",
|
|
1522
|
-
"extensions.slot.sidebar.nav": "侧边栏导航",
|
|
1523
|
-
"extensions.slot.sidebar.nav.top": "侧边栏导航(顶部)",
|
|
1524
|
-
"extensions.slot.sidebar.nav.bottom": "侧边栏导航(底部)",
|
|
1525
|
-
"extensions.slot.sidebar.footer": "侧边栏底部",
|
|
1526
|
-
"extensions.slot.main.workspace": "主工作区",
|
|
1527
|
-
"extensions.slot.session.aside": "会话侧栏",
|
|
1528
|
-
"extensions.slot.settings.tabs": "设置页标签",
|
|
1529
1517
|
"extensions.section.agents": "Agent",
|
|
1530
1518
|
"extensions.section.skills": "技能",
|
|
1531
1519
|
"extensions.section.panels": "面板",
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -128,7 +128,28 @@
|
|
|
128
128
|
<div class="sidebar-divider">
|
|
129
129
|
<span data-i18n="sidebar.chat">Sessions</span>
|
|
130
130
|
<div class="sidebar-divider-actions">
|
|
131
|
-
<
|
|
131
|
+
<div class="btn-split-wrap">
|
|
132
|
+
<button id="btn-new-session-inline" class="btn-split-main" title="New Session">
|
|
133
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
134
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
|
135
|
+
<line x1="12" y1="8" x2="12" y2="14" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
|
|
136
|
+
<line x1="9" y1="11" x2="15" y2="11" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
|
|
137
|
+
</svg>
|
|
138
|
+
<span data-i18n="sessions.newSession">New Session</span>
|
|
139
|
+
</button>
|
|
140
|
+
<div class="btn-split-dropdown">
|
|
141
|
+
<button id="btn-new-session-advanced" class="btn-split-arrow" aria-label="Advanced new session options">
|
|
142
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
143
|
+
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
144
|
+
</svg>
|
|
145
|
+
</button>
|
|
146
|
+
<div class="btn-split-menu">
|
|
147
|
+
<button class="btn-split-menu-item" id="btn-new-session-goto-advanced">
|
|
148
|
+
<span data-i18n="sessions.advancedOptions">Advanced options</span>
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
132
153
|
</div>
|
|
133
154
|
</div>
|
|
134
155
|
|
|
@@ -664,9 +685,9 @@
|
|
|
664
685
|
<div class="extensions-toolbar-right">
|
|
665
686
|
<input type="text" id="extensions-search-input" class="extensions-search" placeholder="Search extensions…" />
|
|
666
687
|
<select id="extensions-sort" class="extensions-sort">
|
|
688
|
+
<option value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</option>
|
|
667
689
|
<option value="newest" data-i18n="extensions.sort.newest">Newest</option>
|
|
668
690
|
<option value="updated" data-i18n="extensions.sort.updated">Recently updated</option>
|
|
669
|
-
<option value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</option>
|
|
670
691
|
</select>
|
|
671
692
|
</div>
|
|
672
693
|
</div>
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -487,8 +487,8 @@ const Sessions = (() => {
|
|
|
487
487
|
// ── New session controls (split button + welcome + modal) ──────────────
|
|
488
488
|
//
|
|
489
489
|
// Wires up every button/interaction that kicks off session creation:
|
|
490
|
-
// - "+ New Session" inline split-button (quick create)
|
|
491
|
-
// - "▾" arrow button (
|
|
490
|
+
// - "+ New Session" inline split-button (quick create — directly creates a plain session)
|
|
491
|
+
// - "▾" arrow button (navigates to /#new for advanced options)
|
|
492
492
|
// - "+ New Session" big button on the welcome screen
|
|
493
493
|
// - New Session Modal: close / cancel / create / overlay click / browse
|
|
494
494
|
// - Load-more button (rendered dynamically by renderList)
|
|
@@ -497,7 +497,28 @@ const Sessions = (() => {
|
|
|
497
497
|
// we call addEventListener directly (no ?. / no `if` guards). If any is
|
|
498
498
|
// missing, it means HTML and JS drifted and we want the loud error.
|
|
499
499
|
function _initNewSessionControls() {
|
|
500
|
-
|
|
500
|
+
// Main button: directly create a plain session using the last-used agent.
|
|
501
|
+
const _btnNewInline = document.getElementById("btn-new-session-inline");
|
|
502
|
+
_btnNewInline.addEventListener("click", async () => {
|
|
503
|
+
if (_btnNewInline.disabled) return;
|
|
504
|
+
_btnNewInline.disabled = true;
|
|
505
|
+
try {
|
|
506
|
+
const session = await NewSessionStore.createSession({ existingSessions: _sessions });
|
|
507
|
+
if (!session) return;
|
|
508
|
+
NewSessionStore.reset();
|
|
509
|
+
// Add to local list immediately so renderList shows it and findOrFetch
|
|
510
|
+
// can locate it synchronously (without a round-trip to the API).
|
|
511
|
+
if (!_sessions.find(s => s.id === session.id)) {
|
|
512
|
+
_sessions.unshift(session);
|
|
513
|
+
}
|
|
514
|
+
location.hash = `session/${session.id}`;
|
|
515
|
+
} finally {
|
|
516
|
+
_btnNewInline.disabled = false;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Arrow button: hover (CSS) shows menu. Menu item navigates to /#new on click.
|
|
521
|
+
document.getElementById("btn-new-session-goto-advanced")
|
|
501
522
|
.addEventListener("click", () => { location.hash = "#new"; });
|
|
502
523
|
|
|
503
524
|
document.addEventListener("click", (e) => {
|
|
@@ -1358,6 +1379,10 @@ const Sessions = (() => {
|
|
|
1358
1379
|
bubbleHtml += escapeHtml(ev.content || "");
|
|
1359
1380
|
el.innerHTML = bubbleHtml;
|
|
1360
1381
|
if (ev.created_at) el.dataset.createdAt = ev.created_at;
|
|
1382
|
+
// Messages archived into a compressed chunk can't be edited (the backend
|
|
1383
|
+
// truncate keys off the active in-memory history). Flag them so the edit
|
|
1384
|
+
// affordance stays hidden.
|
|
1385
|
+
if (ev.editable === false) el.dataset.editable = "false";
|
|
1361
1386
|
const wrap = document.createElement("div");
|
|
1362
1387
|
wrap.className = "msg-user-wrap";
|
|
1363
1388
|
wrap.appendChild(el);
|
|
@@ -1757,7 +1782,9 @@ const Sessions = (() => {
|
|
|
1757
1782
|
});
|
|
1758
1783
|
|
|
1759
1784
|
bar.appendChild(copyBtn);
|
|
1760
|
-
|
|
1785
|
+
// Skip the edit affordance for messages already archived into a compressed
|
|
1786
|
+
// chunk — editing them would silently no-op on the backend.
|
|
1787
|
+
if (el.dataset.editable !== "false") bar.appendChild(editBtn);
|
|
1761
1788
|
wrap.appendChild(bar);
|
|
1762
1789
|
}
|
|
1763
1790
|
|
|
@@ -1811,8 +1838,9 @@ const Sessions = (() => {
|
|
|
1811
1838
|
sendBtn.textContent = I18n.t("chat.send");
|
|
1812
1839
|
sendBtn.addEventListener("click", () => _submitEdit(el, textarea.value.trim()));
|
|
1813
1840
|
|
|
1841
|
+
const editIme = IME.track(textarea);
|
|
1814
1842
|
textarea.addEventListener("keydown", (e) => {
|
|
1815
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
1843
|
+
if (e.key === "Enter" && !e.shiftKey && !editIme.isComposing(e)) {
|
|
1816
1844
|
e.preventDefault();
|
|
1817
1845
|
_submitEdit(el, textarea.value.trim());
|
|
1818
1846
|
}
|
data/scripts/uninstall.sh
CHANGED
|
@@ -227,7 +227,7 @@ uninstall_gem() {
|
|
|
227
227
|
command_exists gem || return 1
|
|
228
228
|
if gem list -i openclacky >/dev/null 2>&1; then
|
|
229
229
|
print_step "Uninstalling via RubyGems..."
|
|
230
|
-
gem uninstall openclacky -x
|
|
230
|
+
gem uninstall openclacky -a -x
|
|
231
231
|
else
|
|
232
232
|
print_info "Gem 'openclacky' not found (already removed)"
|
|
233
233
|
fi
|