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.
@@ -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
- <div class="extension-desc">${escapeHtml(description)}</div>
113
- <div class="extension-meta">
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
- panel.innerHTML = _detailShell(`
151
- <div class="extensions-empty">${escapeHtml(st.detailError)}</div>`);
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">${escapeHtml(I18n.t("extensions.installed"))}</span>` : "";
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-detail-author">${escapeHtml(ext.author)}</span>` : "";
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
- ${(authorHtml || homepageHtml) ? `<div class="extension-meta">${authorHtml}${homepageHtml}</div>` : ""}
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}`;
@@ -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 your extensions to enhance OpenClacky with agents, skills, and panels.",
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 downloaded",
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.installed": "Installed",
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": "浏览并管理你的扩展,用 Agent、技能与面板增强 OpenClacky。",
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.installed": "已安装",
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": "移除",
@@ -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
- let filePath = decodeURIComponent(link.getAttribute("href").replace(/^file:\/\//, ""));
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;
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: 1.3.9
4
+ version: 1.3.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy