openclacky 1.3.10 → 1.4.0

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.
@@ -77,18 +77,35 @@ const ExtensionsView = (() => {
77
77
  return parts.join(" · ");
78
78
  }
79
79
 
80
+ function _defaultIcon(name, extraClass) {
81
+ const letter = (name || "?")[0].toUpperCase();
82
+ const colors = [
83
+ ["#6366f1","#818cf8"], ["#8b5cf6","#a78bfa"], ["#ec4899","#f472b6"],
84
+ ["#f59e0b","#fbbf24"], ["#10b981","#34d399"], ["#3b82f6","#60a5fa"],
85
+ ["#ef4444","#f87171"], ["#14b8a6","#2dd4bf"],
86
+ ];
87
+ const idx = (name || "").charCodeAt(0) % colors.length;
88
+ const [c1, c2] = colors[idx];
89
+ const gid = `eg-${idx}-${Math.random().toString(36).slice(2,7)}`;
90
+ const cls = extraClass ? `extension-emoji ${extraClass}` : "extension-emoji";
91
+ return `<span class="${cls}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="extension-default-icon"><defs><linearGradient id="${gid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="${c1}"/><stop offset="100%" stop-color="${c2}"/></linearGradient></defs><rect width="32" height="32" rx="8" fill="url(#${gid})"/><text x="16" y="22" text-anchor="middle" font-family="system-ui,sans-serif" font-size="16" font-weight="700" fill="white">${letter}</text></svg></span>`;
92
+ }
93
+
80
94
  function _renderCard(ext) {
81
95
  const currentLang = I18n.lang();
82
- const name = (currentLang === "zh" && ext.name_zh) ? ext.name_zh : ext.name;
96
+ const name = (currentLang === "zh" && ext.display_name_zh) ? ext.display_name_zh : (ext.display_name || ext.name);
83
97
  const description = (currentLang === "zh" && ext.description_zh)
84
98
  ? ext.description_zh
85
99
  : ext.description || "";
86
- const emoji = ext.emoji || "🧩";
100
+ const emojiHtml = ext.emoji ? `<span class="extension-emoji">${escapeHtml(ext.emoji)}</span>` : _defaultIcon(ext.name);
87
101
 
88
102
  const versionHtml = ext.version
89
103
  ? `<span class="extension-version">v${escapeHtml(String(ext.version))}</span>` : "";
104
+ const canUpdate = ext.installed_version && ext.version && ext.installed_version !== ext.version;
90
105
  const installedHtml = ext.installed
91
106
  ? `<span class="extension-installed">${escapeHtml(I18n.t("extensions.installed"))}</span>` : "";
107
+ const updatableHtml = canUpdate
108
+ ? `<span class="extension-updatable">${escapeHtml(I18n.t("extensions.updatable"))}</span>` : "";
92
109
  const unlistedHtml = (ext.unlisted && ext.installed)
93
110
  ? `<span class="extension-unlisted">${escapeHtml(I18n.t("extensions.unlisted"))}</span>` : "";
94
111
  const unitsText = _formatUnits(ext.units);
@@ -98,22 +115,26 @@ const ExtensionsView = (() => {
98
115
  ? `<a class="extension-homepage" href="${escapeHtml(ext.homepage)}" target="_blank" rel="noopener noreferrer">${I18n.t("extensions.homepage")}</a>`
99
116
  : "";
100
117
  const authorHtml = ext.author
101
- ? `<span class="extension-author"${escapeHtml(ext.author)}</span>` : "";
118
+ ? `<span class="extension-author">${escapeHtml(I18n.t("extensions.by"))}${escapeHtml(ext.author)}</span>` : "";
119
+ const installsHtml = ext.download_count > 0
120
+ ? `<span class="extension-installs">${escapeHtml(String(ext.download_count))} ${escapeHtml(I18n.t("extensions.installs"))}</span>` : "";
102
121
 
103
122
  const card = document.createElement("div");
104
123
  card.className = "extension-card extension-card-clickable";
105
124
  card.dataset.extId = ext.id != null ? String(ext.id) : (ext.name || "");
106
125
  card.innerHTML = `
107
126
  <div class="extension-card-main">
108
- <div class="extension-emoji">${escapeHtml(emoji)}</div>
127
+ ${emojiHtml}
109
128
  <div class="extension-info">
110
129
  <div class="extension-title">
111
130
  <span class="extension-name">${escapeHtml(name)}</span>
112
- ${authorHtml}
113
131
  ${versionHtml}
114
132
  ${installedHtml}
133
+ ${updatableHtml}
115
134
  ${unlistedHtml}
116
135
  ${unitsHtml}
136
+ ${authorHtml}
137
+ ${installsHtml}
117
138
  </div>
118
139
  ${description ? `<div class="extension-desc">${escapeHtml(description)}</div>` : ""}
119
140
  ${homepageHtml ? `<div class="extension-meta">${homepageHtml}</div>` : ""}
@@ -202,6 +223,16 @@ const ExtensionsView = (() => {
202
223
  Extensions.install(id);
203
224
  });
204
225
  }
226
+
227
+ const updateBtn = document.querySelector("[data-ext-update]");
228
+ if (updateBtn) {
229
+ updateBtn.addEventListener("click", () => {
230
+ const id = updateBtn.getAttribute("data-ext-update");
231
+ updateBtn.disabled = true;
232
+ updateBtn.textContent = I18n.t("extensions.action.updating");
233
+ Extensions.update(id);
234
+ });
235
+ }
205
236
  }
206
237
 
207
238
  function _backToList() {
@@ -212,16 +243,23 @@ const ExtensionsView = (() => {
212
243
 
213
244
  function _detailContent(ext) {
214
245
  const currentLang = I18n.lang();
215
- const name = (currentLang === "zh" && ext.name_zh) ? ext.name_zh : ext.name;
246
+ const name = (currentLang === "zh" && ext.display_name_zh) ? ext.display_name_zh : (ext.display_name || ext.name);
216
247
  const description = (currentLang === "zh" && ext.description_zh)
217
248
  ? ext.description_zh
218
249
  : ext.description || "";
219
250
  const emoji = ext.emoji || "🧩";
251
+ const detailEmojiHtml = ext.emoji
252
+ ? `<span class="extension-emoji extension-emoji-lg">${escapeHtml(ext.emoji)}</span>`
253
+ : _defaultIcon(ext.name, "extension-emoji-lg");
220
254
 
255
+ const canUpdate = ext.installed && ext.installed_version && ext.version && ext.installed_version !== ext.version;
221
256
  const versionHtml = ext.version
222
257
  ? `<span class="extension-version">v${escapeHtml(String(ext.version))}</span>` : "";
258
+ const installedLabel = canUpdate && ext.installed_version
259
+ ? `${I18n.t("extensions.installed")} v${escapeHtml(String(ext.installed_version))}`
260
+ : I18n.t("extensions.installed");
223
261
  const installedHtml = ext.installed
224
- ? `<span class="extension-installed">${escapeHtml(I18n.t("extensions.installed"))}</span>` : "";
262
+ ? `<span class="extension-installed">${installedLabel}</span>` : "";
225
263
  const unlistedHtml = ext.unlisted
226
264
  ? `<span class="extension-unlisted">${escapeHtml(I18n.t("extensions.unlisted"))}</span>` : "";
227
265
  const unitsText = _formatUnits(ext.units);
@@ -231,26 +269,29 @@ const ExtensionsView = (() => {
231
269
  ? `<a class="extension-homepage" href="${escapeHtml(ext.homepage)}" target="_blank" rel="noopener noreferrer">${I18n.t("extensions.homepage")}</a>`
232
270
  : "";
233
271
  const authorHtml = ext.author
234
- ? `<span class="extension-author"${escapeHtml(ext.author)}</span>` : "";
272
+ ? `<span class="extension-author">${escapeHtml(I18n.t("extensions.by"))}${escapeHtml(ext.author)}</span>` : "";
273
+ const installsHtml = ext.download_count > 0
274
+ ? `<span class="extension-installs">${escapeHtml(String(ext.download_count))} ${escapeHtml(I18n.t("extensions.installs"))}</span>` : "";
235
275
 
236
276
  return `
237
277
  <div class="extension-detail-hero">
238
- <div class="extension-emoji extension-emoji-lg">${escapeHtml(emoji)}</div>
278
+ ${detailEmojiHtml}
239
279
  <div class="extension-detail-heading">
240
280
  <div class="extension-title">
241
281
  <span class="extension-name extension-name-lg">${escapeHtml(name)}</span>
242
- ${authorHtml}
243
282
  ${versionHtml}
244
283
  ${installedHtml}
245
284
  ${unlistedHtml}
246
285
  ${unitsHtml}
286
+ ${authorHtml}
287
+ ${installsHtml}
247
288
  </div>
248
289
  ${description ? `<div class="extension-desc extension-desc-detail">${escapeHtml(description)}</div>` : ""}
249
290
  ${homepageHtml ? `<div class="extension-meta">${homepageHtml}</div>` : ""}
250
291
  ${_renderActions(ext)}
251
292
  </div>
252
293
  </div>
253
- ${_renderContributes(ext.contributes)}
294
+ ${_renderEntryPoints(ext.contributes)}
254
295
  ${_renderVersions(ext.versions)}`;
255
296
  }
256
297
 
@@ -275,9 +316,14 @@ const ExtensionsView = (() => {
275
316
  const removeBtn = ext.removable
276
317
  ? `<button type="button" class="extension-action extension-action-remove" data-ext-remove="${escapeHtml(slug)}">${escapeHtml(I18n.t("extensions.action.remove"))}</button>`
277
318
  : "";
319
+ const canUpdate = ext.installed_version && ext.version && ext.installed_version !== ext.version && ext.download_url;
320
+ const updateBtn = canUpdate
321
+ ? `<button type="button" class="extension-action extension-action-update" data-ext-update="${escapeHtml(id)}">${escapeHtml(I18n.t("extensions.action.update"))}</button>`
322
+ : "";
278
323
  return `
279
324
  <div class="extension-detail-actions">
280
325
  ${disabledBadge}
326
+ ${updateBtn}
281
327
  <button type="button" class="extension-action ${toggleCls}" data-ext-toggle="${escapeHtml(slug)}" data-ext-enabled="${ext.disabled ? "1" : "0"}">${escapeHtml(I18n.t(toggleKey))}</button>
282
328
  ${removeBtn}
283
329
  </div>`;
@@ -328,6 +374,24 @@ const ExtensionsView = (() => {
328
374
  </div>`;
329
375
  }
330
376
 
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
+
331
395
  function _renderVersions(versions) {
332
396
  if (!Array.isArray(versions) || versions.length === 0) return "";
333
397
  const rows = versions.map((v) => {
@@ -336,9 +400,9 @@ const ExtensionsView = (() => {
336
400
  <li class="extension-version-item">
337
401
  <div class="extension-version-row">
338
402
  <span class="extension-version">v${escapeHtml(String(v.version || ""))}</span>
339
- ${date ? `<span class="extension-version-date">${escapeHtml(date)}</span>` : ""}
403
+ ${date ? `<span class="extension-version-separator">-</span><span class="extension-version-date">${escapeHtml(date)}</span>` : ""}
340
404
  </div>
341
- ${v.release_notes ? `<div class="extension-version-notes">${escapeHtml(String(v.release_notes))}</div>` : ""}
405
+ ${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>` : ""}
342
406
  </li>`;
343
407
  }).join("");
344
408
  return `
@@ -16,6 +16,62 @@
16
16
 
17
17
  const SkillsView = (() => {
18
18
  let _domWired = false;
19
+ let _searchQuery = "";
20
+
21
+ // ── Search filtering ─────────────────────────────────────────────────────
22
+
23
+ function _applySearch() {
24
+ const q = _searchQuery.trim().toLowerCase();
25
+ const activeTab = SkillsStore.state.activeTab;
26
+
27
+ if (activeTab === "my-skills") {
28
+ const list = $("skills-list");
29
+ if (!list) return;
30
+ list.querySelectorAll(".skill-card").forEach(card => {
31
+ card.style.display = (!q || card.textContent.toLowerCase().includes(q)) ? "" : "none";
32
+ });
33
+ let noResult = list.querySelector(".skills-search-empty");
34
+ const anyVisible = [...list.querySelectorAll(".skill-card")].some(c => c.style.display !== "none");
35
+ if (q && !anyVisible) {
36
+ if (!noResult) {
37
+ noResult = document.createElement("div");
38
+ noResult.className = "skills-search-empty";
39
+ noResult.textContent = I18n.t("skills.search.empty");
40
+ list.appendChild(noResult);
41
+ }
42
+ } else {
43
+ noResult && noResult.remove();
44
+ }
45
+ } else {
46
+ const list = $("brand-skills-list");
47
+ if (!list) return;
48
+ list.querySelectorAll(".brand-skill-card").forEach(card => {
49
+ card.style.display = (!q || card.textContent.toLowerCase().includes(q)) ? "" : "none";
50
+ });
51
+ let noResult = list.querySelector(".skills-search-empty");
52
+ const anyVisible = [...list.querySelectorAll(".brand-skill-card")].some(c => c.style.display !== "none");
53
+ if (q && !anyVisible) {
54
+ if (!noResult) {
55
+ noResult = document.createElement("div");
56
+ noResult.className = "skills-search-empty";
57
+ noResult.textContent = I18n.t("skills.search.empty");
58
+ list.appendChild(noResult);
59
+ }
60
+ } else {
61
+ noResult && noResult.remove();
62
+ }
63
+ }
64
+ }
65
+
66
+ function _wireSearch() {
67
+ const input = $("skills-search-input");
68
+ if (!input) return;
69
+ input.placeholder = I18n.t("skills.search.placeholder");
70
+ input.addEventListener("input", () => {
71
+ _searchQuery = input.value;
72
+ _applySearch();
73
+ });
74
+ }
19
75
 
20
76
  // ── My Skills rendering ──────────────────────────────────────────────────
21
77
 
@@ -45,6 +101,7 @@ const SkillsView = (() => {
45
101
  console.error("[Skills] _renderSkillCard failed for skill", i, skill.name, e);
46
102
  }
47
103
  });
104
+ _applySearch();
48
105
  }
49
106
 
50
107
  function _renderEmptyState() {
@@ -247,6 +304,7 @@ const SkillsView = (() => {
247
304
  if (freeMode && paidSkillsCount > 0) {
248
305
  container.appendChild(_renderPaidHint(paidSkillsCount));
249
306
  }
307
+ _applySearch();
250
308
  }
251
309
 
252
310
  function _renderPaidHint(paidSkillsCount) {
@@ -383,6 +441,8 @@ const SkillsView = (() => {
383
441
  function _wireDom() {
384
442
  if (_domWired) return;
385
443
 
444
+ _wireSearch();
445
+
386
446
  document.querySelectorAll(".skills-tab").forEach(btn => {
387
447
  btn.addEventListener("click", () => Skills.setActiveTab(btn.dataset.tab));
388
448
  });
@@ -430,7 +490,7 @@ const SkillsView = (() => {
430
490
  _renderBrandSkills();
431
491
  });
432
492
 
433
- Skills.on("tab:changed", (p) => _applyTab(p.tab));
493
+ Skills.on("tab:changed", (p) => { _applyTab(p.tab); _applySearch(); });
434
494
 
435
495
  Skills.on("brandStatus:changed", (p) => {
436
496
  const brandTab = $("tab-brand-skills");
@@ -95,6 +95,8 @@ const I18n = (() => {
95
95
  "sessions.deleteTitle": "Delete session",
96
96
  "sessions.createError": "Error: ",
97
97
  "sessions.dirNotEmpty": "Directory already exists and is not empty.",
98
+ "sessions.today": "Today",
99
+ "sessions.yesterday": "Yesterday",
98
100
  "sessions.export.tooltip": "Download session files (session.json + chunks + today's log) for debugging",
99
101
  "sessions.export.failed": "Failed to download session",
100
102
  "sessions.actions.tooltip": "Click for session actions",
@@ -414,6 +416,8 @@ const I18n = (() => {
414
416
  "skills.import.install": "Install",
415
417
  "skills.tab.my": "My Skills",
416
418
  "skills.tab.brand": "Brand Skills",
419
+ "skills.search.placeholder": "Search skills…",
420
+ "skills.search.empty": "No skills match your search.",
417
421
  "skills.empty": "No skills loaded.",
418
422
  "skills.empty.createBtn": "Create a new skill with /skill-creator",
419
423
  "skills.noSkills": "No skills",
@@ -512,15 +516,17 @@ const I18n = (() => {
512
516
  "extensions.searchPlaceholder": "Search extensions…",
513
517
  "extensions.sort.newest": "Newest",
514
518
  "extensions.sort.updated": "Recently updated",
515
- "extensions.sort.downloads": "Most downloaded",
519
+ "extensions.sort.downloads": "Most installed",
516
520
  "extensions.filter.all": "All",
517
521
  "extensions.filter.installed": "Installed",
518
522
  "extensions.empty": "No extensions available yet.",
519
523
  "extensions.noResults": "No extensions match your search.",
520
524
  "extensions.loadFailed": "Could not reach the extension store.",
521
525
  "extensions.homepage": "Homepage",
522
- "extensions.by": "by",
523
- "extensions.installed": "Installed",
526
+ "extensions.by": "by ",
527
+ "extensions.installs": "installs",
528
+ "extensions.installed": "Installed",
529
+ "extensions.updatable": "Update available",
524
530
  "extensions.unlisted": "Unlisted",
525
531
  "extensions.unit.skill": "skill",
526
532
  "extensions.unit.skills": "skills",
@@ -532,7 +538,15 @@ const I18n = (() => {
532
538
  "extensions.unit.apis": "APIs",
533
539
  "extensions.detail.back": "Back",
534
540
  "extensions.detail.contributes": "What's inside",
541
+ "extensions.detail.entry_points": "Access points",
535
542
  "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",
536
550
  "extensions.section.agents": "Agents",
537
551
  "extensions.section.skills": "Skills",
538
552
  "extensions.section.panels": "Panels",
@@ -540,6 +554,8 @@ const I18n = (() => {
540
554
  "extensions.disabled": "Disabled",
541
555
  "extensions.action.install": "Install",
542
556
  "extensions.action.installing": "Installing…",
557
+ "extensions.action.update": "Update",
558
+ "extensions.action.updating": "Updating…",
543
559
  "extensions.action.disable": "Disable",
544
560
  "extensions.action.enable": "Enable",
545
561
  "extensions.action.remove": "Remove",
@@ -1059,6 +1075,8 @@ const I18n = (() => {
1059
1075
  "sessions.deleteTitle": "删除会话",
1060
1076
  "sessions.createError": "错误:",
1061
1077
  "sessions.dirNotEmpty": "该目录已存在且不为空,请换一个目录名。",
1078
+ "sessions.today": "今天",
1079
+ "sessions.yesterday": "昨天",
1062
1080
  "sessions.export.tooltip": "下载会话文件(session.json + 归档片段 + 当天日志),用于调试",
1063
1081
  "sessions.export.failed": "下载会话文件失败",
1064
1082
  "sessions.actions.tooltip": "点击查看会话操作",
@@ -1377,6 +1395,8 @@ const I18n = (() => {
1377
1395
  "skills.import.install": "安装",
1378
1396
  "skills.tab.my": "我的技能",
1379
1397
  "skills.tab.brand": "品牌技能",
1398
+ "skills.search.placeholder": "搜索技能…",
1399
+ "skills.search.empty": "没有匹配的技能。",
1380
1400
  "skills.empty": "暂无技能。",
1381
1401
  "skills.empty.createBtn": "用 /skill-creator 创建新技能",
1382
1402
  "skills.noSkills": "无技能",
@@ -1475,15 +1495,17 @@ const I18n = (() => {
1475
1495
  "extensions.searchPlaceholder": "搜索扩展…",
1476
1496
  "extensions.sort.newest": "最新",
1477
1497
  "extensions.sort.updated": "最近更新",
1478
- "extensions.sort.downloads": "下载最多",
1498
+ "extensions.sort.downloads": "安装最多",
1479
1499
  "extensions.filter.all": "全部",
1480
1500
  "extensions.filter.installed": "已安装",
1481
1501
  "extensions.empty": "暂无可用扩展。",
1482
1502
  "extensions.noResults": "没有匹配的扩展。",
1483
1503
  "extensions.loadFailed": "无法连接扩展市场。",
1484
1504
  "extensions.homepage": "主页",
1485
- "extensions.by": "作者",
1486
- "extensions.installed": "已安装",
1505
+ "extensions.by": "作者:",
1506
+ "extensions.installs": "次安装",
1507
+ "extensions.installed": "已安装",
1508
+ "extensions.updatable": "有更新",
1487
1509
  "extensions.unlisted": "已下架",
1488
1510
  "extensions.unit.skill": "个技能",
1489
1511
  "extensions.unit.skills": "个技能",
@@ -1495,7 +1517,15 @@ const I18n = (() => {
1495
1517
  "extensions.unit.apis": "个 API",
1496
1518
  "extensions.detail.back": "返回",
1497
1519
  "extensions.detail.contributes": "包含内容",
1520
+ "extensions.detail.entry_points": "访问入口",
1498
1521
  "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": "设置页标签",
1499
1529
  "extensions.section.agents": "Agent",
1500
1530
  "extensions.section.skills": "技能",
1501
1531
  "extensions.section.panels": "面板",
@@ -1503,6 +1533,8 @@ const I18n = (() => {
1503
1533
  "extensions.disabled": "已禁用",
1504
1534
  "extensions.action.install": "安装",
1505
1535
  "extensions.action.installing": "安装中…",
1536
+ "extensions.action.update": "更新",
1537
+ "extensions.action.updating": "更新中…",
1506
1538
  "extensions.action.disable": "禁用",
1507
1539
  "extensions.action.enable": "启用",
1508
1540
  "extensions.action.remove": "移除",
@@ -46,21 +46,14 @@
46
46
  </div>
47
47
  <div id="header-center">
48
48
  <button id="header-cmdbar" type="button" title="Search sessions">
49
- <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
49
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.3-4.3"/></svg>
50
50
  <span class="cmdbar-ph" data-i18n="header.cmdbar.placeholder">Search sessions…</span>
51
- <span class="cmdbar-kbd">⌘K</span>
51
+ <span class="cmdbar-kbd">Ctrl + K</span>
52
52
  </button>
53
53
  </div>
54
54
  <div id="header-right">
55
55
  <span id="ext-slot-header-right" data-slot="header.right"></span>
56
- <button id="refresh-page-btn" class="theme-toggle-btn" data-i18n-title="header.refresh" title="Refresh" onclick="location.reload()">
57
- <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">
58
- <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
59
- <path d="M21 3v5h-5"/>
60
- <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
61
- <path d="M8 16H3v5"/>
62
- </svg>
63
- </button>
56
+
64
57
  <button id="share-toggle-header" class="theme-toggle-btn" data-i18n-title="share.tooltip" title="Share">
65
58
  <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
59
  <circle cx="18" cy="5" r="3"/>
@@ -79,7 +72,7 @@
79
72
  </div>
80
73
  </header>
81
74
 
82
- <!-- ── Command-palette search overlay (⌘K / top cmdbar) ─────────────────── -->
75
+ <!-- ── Command-palette search overlay (platform shortcut / top cmdbar) ───── -->
83
76
  <!-- Search lives here, decoupled from the sidebar list: results render in
84
77
  #session-search-results and the left session list is never replaced. -->
85
78
  <div id="session-search-overlay" class="cmd-palette-overlay" hidden>
@@ -573,6 +566,8 @@
573
566
  <button class="skills-tab" data-tab="brand-skills" id="tab-brand-skills" data-i18n="skills.tab.brand">Brand Skills</button>
574
567
  </div>
575
568
  <div class="skills-tabs-controls">
569
+ <!-- Search input -->
570
+ <input type="text" id="skills-search-input" class="skills-search" placeholder="Search skills…" data-i18n-placeholder="skills.search.placeholder" />
576
571
  <!-- Show system skills toggle (only visible in My Skills tab) -->
577
572
  <label class="skills-filter-toggle" id="label-show-system">
578
573
  <input type="checkbox" id="chk-show-system-skills">
@@ -884,8 +884,17 @@ const Sessions = (() => {
884
884
  //
885
885
  // Everything uses event delegation because some elements (e.g. the clear
886
886
  // buttons) are re-rendered as filter state changes.
887
+ function _sessionSearchShortcutLabel() {
888
+ const uaDataPlatform = navigator.userAgentData && navigator.userAgentData.platform;
889
+ const platform = (uaDataPlatform || navigator.platform || "").toString();
890
+ return /mac/i.test(platform) ? "⌘ + K" : "Ctrl + K";
891
+ }
892
+
887
893
  function _initSearch() {
888
- // Open the palette: top cmdbar button (or ⌘K, bound below).
894
+ const cmdbarKbd = document.querySelector("#header-cmdbar .cmdbar-kbd");
895
+ if (cmdbarKbd) cmdbarKbd.textContent = _sessionSearchShortcutLabel();
896
+
897
+ // Open the palette: top cmdbar button (or the keyboard shortcut, bound below).
889
898
  document.addEventListener("click", (e) => {
890
899
  if (e.target && e.target.closest("#header-cmdbar")) {
891
900
  if (!Sessions.searchOpen) Sessions.toggleSearch();
@@ -1660,8 +1669,8 @@ const Sessions = (() => {
1660
1669
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1661
1670
  const dDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
1662
1671
  const diffDays = Math.round((today - dDay) / 86400000);
1663
- if (diffDays === 0) return `Today ${hhmm}`;
1664
- if (diffDays === 1) return `Yesterday ${hhmm}`;
1672
+ if (diffDays === 0) return `${I18n.t("sessions.today")} ${hhmm}`;
1673
+ if (diffDays === 1) return `${I18n.t("sessions.yesterday")} ${hhmm}`;
1665
1674
  return `${d.getMonth() + 1}/${d.getDate()} ${hhmm}`;
1666
1675
  }
1667
1676
 
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.10
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -480,6 +480,7 @@ files:
480
480
  - lib/clacky/media/gemini.rb
481
481
  - lib/clacky/media/generator.rb
482
482
  - lib/clacky/media/openai_compat.rb
483
+ - lib/clacky/media/volcengine.rb
483
484
  - lib/clacky/message_format/anthropic.rb
484
485
  - lib/clacky/message_format/bedrock.rb
485
486
  - lib/clacky/message_format/open_ai.rb