openclacky 1.3.9 → 1.3.11
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 +40 -0
- data/lib/clacky/brand_config.rb +32 -6
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +47 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +167 -80
- data/lib/clacky/extension/packager.rb +2 -3
- data/lib/clacky/locales/en.rb +1 -1
- data/lib/clacky/locales/zh.rb +1 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/server/http_server.rb +119 -13
- data/lib/clacky/telemetry.rb +9 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +99 -14
- data/lib/clacky/web/features/extensions/store.js +34 -9
- data/lib/clacky/web/features/extensions/view.js +47 -11
- data/lib/clacky/web/features/new-session/view.js +1 -1
- data/lib/clacky/web/i18n.js +18 -6
- data/lib/clacky/web/sessions.js +32 -2
- metadata +1 -1
|
@@ -87,14 +87,23 @@ const ExtensionsView = (() => {
|
|
|
87
87
|
|
|
88
88
|
const versionHtml = ext.version
|
|
89
89
|
? `<span class="extension-version">v${escapeHtml(String(ext.version))}</span>` : "";
|
|
90
|
+
const canUpdate = ext.installed_version && ext.version && ext.installed_version !== ext.version;
|
|
90
91
|
const installedHtml = ext.installed
|
|
91
92
|
? `<span class="extension-installed">${escapeHtml(I18n.t("extensions.installed"))}</span>` : "";
|
|
93
|
+
const updatableHtml = canUpdate
|
|
94
|
+
? `<span class="extension-updatable">${escapeHtml(I18n.t("extensions.updatable"))}</span>` : "";
|
|
95
|
+
const unlistedHtml = (ext.unlisted && ext.installed)
|
|
96
|
+
? `<span class="extension-unlisted">${escapeHtml(I18n.t("extensions.unlisted"))}</span>` : "";
|
|
92
97
|
const unitsText = _formatUnits(ext.units);
|
|
93
98
|
const unitsHtml = unitsText
|
|
94
99
|
? `<span class="extension-units">${escapeHtml(unitsText)}</span>` : "";
|
|
95
100
|
const homepageHtml = ext.homepage
|
|
96
101
|
? `<a class="extension-homepage" href="${escapeHtml(ext.homepage)}" target="_blank" rel="noopener noreferrer">${I18n.t("extensions.homepage")}</a>`
|
|
97
102
|
: "";
|
|
103
|
+
const authorHtml = ext.author
|
|
104
|
+
? `<span class="extension-author">${escapeHtml(I18n.t("extensions.by"))}${escapeHtml(ext.author)}</span>` : "";
|
|
105
|
+
const installsHtml = ext.download_count > 0
|
|
106
|
+
? `<span class="extension-installs">${escapeHtml(String(ext.download_count))} ${escapeHtml(I18n.t("extensions.installs"))}</span>` : "";
|
|
98
107
|
|
|
99
108
|
const card = document.createElement("div");
|
|
100
109
|
card.className = "extension-card extension-card-clickable";
|
|
@@ -107,12 +116,14 @@ const ExtensionsView = (() => {
|
|
|
107
116
|
<span class="extension-name">${escapeHtml(name)}</span>
|
|
108
117
|
${versionHtml}
|
|
109
118
|
${installedHtml}
|
|
119
|
+
${updatableHtml}
|
|
120
|
+
${unlistedHtml}
|
|
110
121
|
${unitsHtml}
|
|
122
|
+
${authorHtml}
|
|
123
|
+
${installsHtml}
|
|
111
124
|
</div>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
${homepageHtml}
|
|
115
|
-
</div>
|
|
125
|
+
${description ? `<div class="extension-desc">${escapeHtml(description)}</div>` : ""}
|
|
126
|
+
${homepageHtml ? `<div class="extension-meta">${homepageHtml}</div>` : ""}
|
|
116
127
|
</div>
|
|
117
128
|
</div>`;
|
|
118
129
|
return card;
|
|
@@ -147,9 +158,8 @@ const ExtensionsView = (() => {
|
|
|
147
158
|
}
|
|
148
159
|
|
|
149
160
|
if (st.detailError) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
_wireDetail();
|
|
161
|
+
console.warn("[Extensions] detail error, navigating back:", st.detailError);
|
|
162
|
+
_backToList();
|
|
153
163
|
return;
|
|
154
164
|
}
|
|
155
165
|
|
|
@@ -199,6 +209,16 @@ const ExtensionsView = (() => {
|
|
|
199
209
|
Extensions.install(id);
|
|
200
210
|
});
|
|
201
211
|
}
|
|
212
|
+
|
|
213
|
+
const updateBtn = document.querySelector("[data-ext-update]");
|
|
214
|
+
if (updateBtn) {
|
|
215
|
+
updateBtn.addEventListener("click", () => {
|
|
216
|
+
const id = updateBtn.getAttribute("data-ext-update");
|
|
217
|
+
updateBtn.disabled = true;
|
|
218
|
+
updateBtn.textContent = I18n.t("extensions.action.updating");
|
|
219
|
+
Extensions.update(id);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
202
222
|
}
|
|
203
223
|
|
|
204
224
|
function _backToList() {
|
|
@@ -215,10 +235,16 @@ const ExtensionsView = (() => {
|
|
|
215
235
|
: ext.description || "";
|
|
216
236
|
const emoji = ext.emoji || "🧩";
|
|
217
237
|
|
|
238
|
+
const canUpdate = ext.installed && ext.installed_version && ext.version && ext.installed_version !== ext.version;
|
|
218
239
|
const versionHtml = ext.version
|
|
219
240
|
? `<span class="extension-version">v${escapeHtml(String(ext.version))}</span>` : "";
|
|
241
|
+
const installedLabel = canUpdate && ext.installed_version
|
|
242
|
+
? `${I18n.t("extensions.installed")} v${escapeHtml(String(ext.installed_version))}`
|
|
243
|
+
: I18n.t("extensions.installed");
|
|
220
244
|
const installedHtml = ext.installed
|
|
221
|
-
? `<span class="extension-installed">${
|
|
245
|
+
? `<span class="extension-installed">${installedLabel}</span>` : "";
|
|
246
|
+
const unlistedHtml = ext.unlisted
|
|
247
|
+
? `<span class="extension-unlisted">${escapeHtml(I18n.t("extensions.unlisted"))}</span>` : "";
|
|
222
248
|
const unitsText = _formatUnits(ext.units);
|
|
223
249
|
const unitsHtml = unitsText
|
|
224
250
|
? `<span class="extension-units">${escapeHtml(unitsText)}</span>` : "";
|
|
@@ -226,7 +252,9 @@ const ExtensionsView = (() => {
|
|
|
226
252
|
? `<a class="extension-homepage" href="${escapeHtml(ext.homepage)}" target="_blank" rel="noopener noreferrer">${I18n.t("extensions.homepage")}</a>`
|
|
227
253
|
: "";
|
|
228
254
|
const authorHtml = ext.author
|
|
229
|
-
? `<span class="extension-
|
|
255
|
+
? `<span class="extension-author">${escapeHtml(I18n.t("extensions.by"))}${escapeHtml(ext.author)}</span>` : "";
|
|
256
|
+
const installsHtml = ext.download_count > 0
|
|
257
|
+
? `<span class="extension-installs">${escapeHtml(String(ext.download_count))} ${escapeHtml(I18n.t("extensions.installs"))}</span>` : "";
|
|
230
258
|
|
|
231
259
|
return `
|
|
232
260
|
<div class="extension-detail-hero">
|
|
@@ -236,10 +264,13 @@ const ExtensionsView = (() => {
|
|
|
236
264
|
<span class="extension-name extension-name-lg">${escapeHtml(name)}</span>
|
|
237
265
|
${versionHtml}
|
|
238
266
|
${installedHtml}
|
|
267
|
+
${unlistedHtml}
|
|
239
268
|
${unitsHtml}
|
|
269
|
+
${authorHtml}
|
|
270
|
+
${installsHtml}
|
|
240
271
|
</div>
|
|
241
272
|
${description ? `<div class="extension-desc extension-desc-detail">${escapeHtml(description)}</div>` : ""}
|
|
242
|
-
${
|
|
273
|
+
${homepageHtml ? `<div class="extension-meta">${homepageHtml}</div>` : ""}
|
|
243
274
|
${_renderActions(ext)}
|
|
244
275
|
</div>
|
|
245
276
|
</div>
|
|
@@ -268,9 +299,14 @@ const ExtensionsView = (() => {
|
|
|
268
299
|
const removeBtn = ext.removable
|
|
269
300
|
? `<button type="button" class="extension-action extension-action-remove" data-ext-remove="${escapeHtml(slug)}">${escapeHtml(I18n.t("extensions.action.remove"))}</button>`
|
|
270
301
|
: "";
|
|
302
|
+
const canUpdate = ext.installed_version && ext.version && ext.installed_version !== ext.version && ext.download_url;
|
|
303
|
+
const updateBtn = canUpdate
|
|
304
|
+
? `<button type="button" class="extension-action extension-action-update" data-ext-update="${escapeHtml(id)}">${escapeHtml(I18n.t("extensions.action.update"))}</button>`
|
|
305
|
+
: "";
|
|
271
306
|
return `
|
|
272
307
|
<div class="extension-detail-actions">
|
|
273
308
|
${disabledBadge}
|
|
309
|
+
${updateBtn}
|
|
274
310
|
<button type="button" class="extension-action ${toggleCls}" data-ext-toggle="${escapeHtml(slug)}" data-ext-enabled="${ext.disabled ? "1" : "0"}">${escapeHtml(I18n.t(toggleKey))}</button>
|
|
275
311
|
${removeBtn}
|
|
276
312
|
</div>`;
|
|
@@ -331,7 +367,7 @@ const ExtensionsView = (() => {
|
|
|
331
367
|
<span class="extension-version">v${escapeHtml(String(v.version || ""))}</span>
|
|
332
368
|
${date ? `<span class="extension-version-date">${escapeHtml(date)}</span>` : ""}
|
|
333
369
|
</div>
|
|
334
|
-
${v.release_notes ? `<div class="extension-version-notes">${escapeHtml(String(v.release_notes))}</div>` : ""}
|
|
370
|
+
${v.release_notes ? `<div class="extension-version-notes">${typeof marked !== "undefined" ? marked.parse(String(v.release_notes).replace(/^#{1,3}[^\n]*\n?/, ""), { breaks: true, gfm: true }) : escapeHtml(String(v.release_notes))}</div>` : ""}
|
|
335
371
|
</li>`;
|
|
336
372
|
}).join("");
|
|
337
373
|
return `
|
|
@@ -110,7 +110,7 @@ const NewSessionView = (() => {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const author = (a.author || "").trim();
|
|
113
|
-
if (author) {
|
|
113
|
+
if (author && !_isBuiltin(a)) {
|
|
114
114
|
const by = document.createElement("div");
|
|
115
115
|
by.className = "agent-card-author";
|
|
116
116
|
by.textContent = _isZh() ? `作者 ${author}` : `by ${author}`;
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -508,18 +508,22 @@ const I18n = (() => {
|
|
|
508
508
|
"mcp.title": "MCP Servers",
|
|
509
509
|
"mcp.subtitle": "Standard Model Context Protocol servers (Claude Desktop / Cursor compatible). Edit ~/.clacky/mcp.json to add or remove servers.",
|
|
510
510
|
"extensions.title": "Extension Market",
|
|
511
|
-
"extensions.subtitle": "Browse and manage
|
|
511
|
+
"extensions.subtitle": "Browse and manage extensions — add agents, skills, and panels to supercharge your workflow.",
|
|
512
512
|
"extensions.searchPlaceholder": "Search extensions…",
|
|
513
513
|
"extensions.sort.newest": "Newest",
|
|
514
514
|
"extensions.sort.updated": "Recently updated",
|
|
515
|
-
"extensions.sort.downloads": "Most
|
|
515
|
+
"extensions.sort.downloads": "Most installed",
|
|
516
516
|
"extensions.filter.all": "All",
|
|
517
517
|
"extensions.filter.installed": "Installed",
|
|
518
518
|
"extensions.empty": "No extensions available yet.",
|
|
519
519
|
"extensions.noResults": "No extensions match your search.",
|
|
520
520
|
"extensions.loadFailed": "Could not reach the extension store.",
|
|
521
521
|
"extensions.homepage": "Homepage",
|
|
522
|
-
"extensions.
|
|
522
|
+
"extensions.by": "by ",
|
|
523
|
+
"extensions.installs": "installs",
|
|
524
|
+
"extensions.installed": "Installed",
|
|
525
|
+
"extensions.updatable": "Update available",
|
|
526
|
+
"extensions.unlisted": "Unlisted",
|
|
523
527
|
"extensions.unit.skill": "skill",
|
|
524
528
|
"extensions.unit.skills": "skills",
|
|
525
529
|
"extensions.unit.agent": "agent",
|
|
@@ -538,6 +542,8 @@ const I18n = (() => {
|
|
|
538
542
|
"extensions.disabled": "Disabled",
|
|
539
543
|
"extensions.action.install": "Install",
|
|
540
544
|
"extensions.action.installing": "Installing…",
|
|
545
|
+
"extensions.action.update": "Update",
|
|
546
|
+
"extensions.action.updating": "Updating…",
|
|
541
547
|
"extensions.action.disable": "Disable",
|
|
542
548
|
"extensions.action.enable": "Enable",
|
|
543
549
|
"extensions.action.remove": "Remove",
|
|
@@ -1469,18 +1475,22 @@ const I18n = (() => {
|
|
|
1469
1475
|
"mcp.title": "MCP 工具",
|
|
1470
1476
|
"mcp.subtitle": "标准 Model Context Protocol 服务(与 Claude Desktop / Cursor 兼容)。编辑 ~/.clacky/mcp.json 添加或移除服务。",
|
|
1471
1477
|
"extensions.title": "扩展市场",
|
|
1472
|
-
"extensions.subtitle": "
|
|
1478
|
+
"extensions.subtitle": "浏览并管理扩展,用 Agent、技能与面板增强你的工作流。",
|
|
1473
1479
|
"extensions.searchPlaceholder": "搜索扩展…",
|
|
1474
1480
|
"extensions.sort.newest": "最新",
|
|
1475
1481
|
"extensions.sort.updated": "最近更新",
|
|
1476
|
-
"extensions.sort.downloads": "
|
|
1482
|
+
"extensions.sort.downloads": "安装最多",
|
|
1477
1483
|
"extensions.filter.all": "全部",
|
|
1478
1484
|
"extensions.filter.installed": "已安装",
|
|
1479
1485
|
"extensions.empty": "暂无可用扩展。",
|
|
1480
1486
|
"extensions.noResults": "没有匹配的扩展。",
|
|
1481
1487
|
"extensions.loadFailed": "无法连接扩展市场。",
|
|
1482
1488
|
"extensions.homepage": "主页",
|
|
1483
|
-
"extensions.
|
|
1489
|
+
"extensions.by": "作者:",
|
|
1490
|
+
"extensions.installs": "次安装",
|
|
1491
|
+
"extensions.installed": "已安装",
|
|
1492
|
+
"extensions.updatable": "有更新",
|
|
1493
|
+
"extensions.unlisted": "已下架",
|
|
1484
1494
|
"extensions.unit.skill": "个技能",
|
|
1485
1495
|
"extensions.unit.skills": "个技能",
|
|
1486
1496
|
"extensions.unit.agent": "个 Agent",
|
|
@@ -1499,6 +1509,8 @@ const I18n = (() => {
|
|
|
1499
1509
|
"extensions.disabled": "已禁用",
|
|
1500
1510
|
"extensions.action.install": "安装",
|
|
1501
1511
|
"extensions.action.installing": "安装中…",
|
|
1512
|
+
"extensions.action.update": "更新",
|
|
1513
|
+
"extensions.action.updating": "更新中…",
|
|
1502
1514
|
"extensions.action.disable": "禁用",
|
|
1503
1515
|
"extensions.action.enable": "启用",
|
|
1504
1516
|
"extensions.action.remove": "移除",
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -113,6 +113,33 @@ const Sessions = (() => {
|
|
|
113
113
|
return html;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Encode file:// URLs inside raw markdown BEFORE marked parses it. The AI
|
|
117
|
+
// emits raw paths (spaces / non-ASCII / % literal); marked's link tokenizer
|
|
118
|
+
// treats a space as the end of the URL, so links with spaces are never
|
|
119
|
+
// recognized. Re-encoding the path (decode then encode, idempotent) turns
|
|
120
|
+
// spaces into %20 and a literal % into %25 so marked sees a valid URL.
|
|
121
|
+
function _encodeFileUrlsInMarkdown(text) {
|
|
122
|
+
return text.replace(
|
|
123
|
+
/(!?\[[^\]]*\]\()(file:\/{2,3})([^)]+)(\))/g,
|
|
124
|
+
(_m, open, scheme, path, close) => {
|
|
125
|
+
try { path = decodeURI(path); } catch (_) { /* keep raw if malformed */ }
|
|
126
|
+
return open + scheme + encodeURI(path) + close;
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Normalize a file:// href for use in <a href>. The AI emits raw paths
|
|
132
|
+
// (Chinese chars / spaces literal); encode them to a valid percent-encoded
|
|
133
|
+
// URL. Idempotent: an already-encoded path is decoded first so it isn't
|
|
134
|
+
// double-encoded. Non file:// links pass through untouched.
|
|
135
|
+
function _normalizeFileHref(href) {
|
|
136
|
+
if (typeof href !== "string" || !href.startsWith("file://")) return href;
|
|
137
|
+
const prefix = href.match(/^file:\/{2,3}/)[0];
|
|
138
|
+
let path = href.slice(prefix.length);
|
|
139
|
+
try { path = decodeURI(path); } catch (_) { /* keep raw if malformed */ }
|
|
140
|
+
return prefix + encodeURI(path);
|
|
141
|
+
}
|
|
142
|
+
|
|
116
143
|
// Run marked on a text string. Returns HTML. Falls back to escaped plain text
|
|
117
144
|
// if the marked library is unavailable.
|
|
118
145
|
function _markedParse(text) {
|
|
@@ -122,6 +149,7 @@ const Sessions = (() => {
|
|
|
122
149
|
const math = [];
|
|
123
150
|
const PLACEHOLDER = (i) => `\u0000KTX${i}\u0000`;
|
|
124
151
|
let prepared = _extractMath(text, math, PLACEHOLDER);
|
|
152
|
+
prepared = _encodeFileUrlsInMarkdown(prepared);
|
|
125
153
|
|
|
126
154
|
let html;
|
|
127
155
|
if (typeof marked !== "undefined") {
|
|
@@ -138,7 +166,7 @@ const Sessions = (() => {
|
|
|
138
166
|
const renderer = new marked.Renderer();
|
|
139
167
|
renderer.link = function({ href, title, text }) {
|
|
140
168
|
const titleAttr = title ? ` title="${title}"` : "";
|
|
141
|
-
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
169
|
+
return `<a href="${_normalizeFileHref(href)}"${titleAttr} target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
142
170
|
};
|
|
143
171
|
// Override code block rendering: apply syntax highlighting + header with
|
|
144
172
|
// language label and copy button.
|
|
@@ -2307,7 +2335,9 @@ const Sessions = (() => {
|
|
|
2307
2335
|
const link = e.target.closest("a[href^='file://']");
|
|
2308
2336
|
if (!link) return;
|
|
2309
2337
|
e.preventDefault();
|
|
2310
|
-
|
|
2338
|
+
const rawHref = link.getAttribute("href").replace(/^file:\/\//, "");
|
|
2339
|
+
let filePath;
|
|
2340
|
+
try { filePath = decodeURIComponent(rawHref); } catch (_) { filePath = rawHref; }
|
|
2311
2341
|
// file:///C:/foo → /C:/foo after replace; strip the leading slash for Windows drive letters
|
|
2312
2342
|
if (/^\/[A-Za-z]:/.test(filePath)) filePath = filePath.substring(1);
|
|
2313
2343
|
if (!filePath) return;
|