openclacky 1.3.11 → 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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/lib/clacky/agent/session_serializer.rb +11 -4
  4. data/lib/clacky/agent/skill_manager.rb +1 -0
  5. data/lib/clacky/agent.rb +4 -5
  6. data/lib/clacky/brand_config.rb +53 -3
  7. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +202 -3
  8. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +610 -60
  9. data/lib/clacky/default_skills/media-gen/SKILL.md +182 -7
  10. data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +5 -3
  11. data/lib/clacky/extension/packager.rb +21 -1
  12. data/lib/clacky/extension/verifier.rb +1 -1
  13. data/lib/clacky/media/generator.rb +41 -0
  14. data/lib/clacky/media/volcengine.rb +394 -0
  15. data/lib/clacky/platform_http_client.rb +9 -7
  16. data/lib/clacky/providers.rb +4 -4
  17. data/lib/clacky/server/http_server.rb +67 -22
  18. data/lib/clacky/tools/grep.rb +6 -1
  19. data/lib/clacky/tools/terminal.rb +24 -8
  20. data/lib/clacky/ui2/layout_manager.rb +4 -0
  21. data/lib/clacky/ui2/screen_buffer.rb +31 -3
  22. data/lib/clacky/utils/environment_detector.rb +16 -9
  23. data/lib/clacky/utils/file_processor.rb +30 -28
  24. data/lib/clacky/utils/model_pricing.rb +49 -0
  25. data/lib/clacky/version.rb +1 -1
  26. data/lib/clacky/web/app.css +229 -31
  27. data/lib/clacky/web/components/onboard.js +14 -4
  28. data/lib/clacky/web/features/backup/view.js +4 -3
  29. data/lib/clacky/web/features/extensions/store.js +2 -2
  30. data/lib/clacky/web/features/extensions/view.js +37 -7
  31. data/lib/clacky/web/features/mcp/view.js +5 -2
  32. data/lib/clacky/web/features/skills/view.js +61 -1
  33. data/lib/clacky/web/features/workspace/store.js +11 -0
  34. data/lib/clacky/web/features/workspace/view.js +10 -4
  35. data/lib/clacky/web/i18n.js +14 -2
  36. data/lib/clacky/web/index.html +29 -13
  37. data/lib/clacky/web/sessions.js +45 -8
  38. data/scripts/uninstall.sh +1 -1
  39. metadata +2 -1
@@ -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");
@@ -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 absPath = Workspace.state.workingDir.replace(/\/+$/, "") + "/" + entry.path.replace(/^\/+/, "");
161
- navigator.clipboard.writeText(absPath).then(() => {
162
- Modal.toast(absPath, "info");
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
 
@@ -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",
@@ -177,7 +179,8 @@ const I18n = (() => {
177
179
  "sessions.badge.channel": "Channel",
178
180
  "sessions.badge.coding": "Coding",
179
181
  "sessions.badge.setup": "Setup",
180
- "sessions.newSession": "+ New Session",
182
+ "sessions.newSession": "New Session",
183
+ "sessions.advancedOptions": "Advanced options",
181
184
  "sessions.actions.pin": "Pin",
182
185
  "sessions.actions.unpin": "Unpin",
183
186
  "sessions.actions.fork": "Fork",
@@ -414,6 +417,8 @@ const I18n = (() => {
414
417
  "skills.import.install": "Install",
415
418
  "skills.tab.my": "My Skills",
416
419
  "skills.tab.brand": "Brand Skills",
420
+ "skills.search.placeholder": "Search skills…",
421
+ "skills.search.empty": "No skills match your search.",
417
422
  "skills.empty": "No skills loaded.",
418
423
  "skills.empty.createBtn": "Create a new skill with /skill-creator",
419
424
  "skills.noSkills": "No skills",
@@ -533,6 +538,7 @@ const I18n = (() => {
533
538
  "extensions.unit.api": "API",
534
539
  "extensions.unit.apis": "APIs",
535
540
  "extensions.detail.back": "Back",
541
+ "extensions.detail.readme": "Description",
536
542
  "extensions.detail.contributes": "What's inside",
537
543
  "extensions.detail.versions": "Version history",
538
544
  "extensions.section.agents": "Agents",
@@ -1063,6 +1069,8 @@ const I18n = (() => {
1063
1069
  "sessions.deleteTitle": "删除会话",
1064
1070
  "sessions.createError": "错误:",
1065
1071
  "sessions.dirNotEmpty": "该目录已存在且不为空,请换一个目录名。",
1072
+ "sessions.today": "今天",
1073
+ "sessions.yesterday": "昨天",
1066
1074
  "sessions.export.tooltip": "下载会话文件(session.json + 归档片段 + 当天日志),用于调试",
1067
1075
  "sessions.export.failed": "下载会话文件失败",
1068
1076
  "sessions.actions.tooltip": "点击查看会话操作",
@@ -1145,7 +1153,8 @@ const I18n = (() => {
1145
1153
  "sessions.badge.channel": "频道",
1146
1154
  "sessions.badge.coding": "Coding",
1147
1155
  "sessions.badge.setup": "配置",
1148
- "sessions.newSession": "+ 新会话",
1156
+ "sessions.newSession": "新会话",
1157
+ "sessions.advancedOptions": "高级选项",
1149
1158
  "sessions.loadMore": "加载更多会话",
1150
1159
  "sessions.actions.pin": "置顶",
1151
1160
  "sessions.actions.unpin": "取消置顶",
@@ -1381,6 +1390,8 @@ const I18n = (() => {
1381
1390
  "skills.import.install": "安装",
1382
1391
  "skills.tab.my": "我的技能",
1383
1392
  "skills.tab.brand": "品牌技能",
1393
+ "skills.search.placeholder": "搜索技能…",
1394
+ "skills.search.empty": "没有匹配的技能。",
1384
1395
  "skills.empty": "暂无技能。",
1385
1396
  "skills.empty.createBtn": "用 /skill-creator 创建新技能",
1386
1397
  "skills.noSkills": "无技能",
@@ -1500,6 +1511,7 @@ const I18n = (() => {
1500
1511
  "extensions.unit.api": "个 API",
1501
1512
  "extensions.unit.apis": "个 API",
1502
1513
  "extensions.detail.back": "返回",
1514
+ "extensions.detail.readme": "使用说明",
1503
1515
  "extensions.detail.contributes": "包含内容",
1504
1516
  "extensions.detail.versions": "版本历史",
1505
1517
  "extensions.section.agents": "Agent",
@@ -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>
@@ -135,7 +128,28 @@
135
128
  <div class="sidebar-divider">
136
129
  <span data-i18n="sidebar.chat">Sessions</span>
137
130
  <div class="sidebar-divider-actions">
138
- <button id="btn-new-session-inline" class="btn-split-main" title="New Session" data-i18n="sessions.newSession">+ New Session</button>
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>
139
153
  </div>
140
154
  </div>
141
155
 
@@ -573,6 +587,8 @@
573
587
  <button class="skills-tab" data-tab="brand-skills" id="tab-brand-skills" data-i18n="skills.tab.brand">Brand Skills</button>
574
588
  </div>
575
589
  <div class="skills-tabs-controls">
590
+ <!-- Search input -->
591
+ <input type="text" id="skills-search-input" class="skills-search" placeholder="Search skills…" data-i18n-placeholder="skills.search.placeholder" />
576
592
  <!-- Show system skills toggle (only visible in My Skills tab) -->
577
593
  <label class="skills-filter-toggle" id="label-show-system">
578
594
  <input type="checkbox" id="chk-show-system-skills">
@@ -669,9 +685,9 @@
669
685
  <div class="extensions-toolbar-right">
670
686
  <input type="text" id="extensions-search-input" class="extensions-search" placeholder="Search extensions…" />
671
687
  <select id="extensions-sort" class="extensions-sort">
688
+ <option value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</option>
672
689
  <option value="newest" data-i18n="extensions.sort.newest">Newest</option>
673
690
  <option value="updated" data-i18n="extensions.sort.updated">Recently updated</option>
674
- <option value="downloads" data-i18n="extensions.sort.downloads">Most downloaded</option>
675
691
  </select>
676
692
  </div>
677
693
  </div>
@@ -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 (opens dropdown advanced options modal)
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
- document.getElementById("btn-new-session-inline")
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) => {
@@ -884,8 +905,17 @@ const Sessions = (() => {
884
905
  //
885
906
  // Everything uses event delegation because some elements (e.g. the clear
886
907
  // buttons) are re-rendered as filter state changes.
908
+ function _sessionSearchShortcutLabel() {
909
+ const uaDataPlatform = navigator.userAgentData && navigator.userAgentData.platform;
910
+ const platform = (uaDataPlatform || navigator.platform || "").toString();
911
+ return /mac/i.test(platform) ? "⌘ + K" : "Ctrl + K";
912
+ }
913
+
887
914
  function _initSearch() {
888
- // Open the palette: top cmdbar button (or ⌘K, bound below).
915
+ const cmdbarKbd = document.querySelector("#header-cmdbar .cmdbar-kbd");
916
+ if (cmdbarKbd) cmdbarKbd.textContent = _sessionSearchShortcutLabel();
917
+
918
+ // Open the palette: top cmdbar button (or the keyboard shortcut, bound below).
889
919
  document.addEventListener("click", (e) => {
890
920
  if (e.target && e.target.closest("#header-cmdbar")) {
891
921
  if (!Sessions.searchOpen) Sessions.toggleSearch();
@@ -1349,6 +1379,10 @@ const Sessions = (() => {
1349
1379
  bubbleHtml += escapeHtml(ev.content || "");
1350
1380
  el.innerHTML = bubbleHtml;
1351
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";
1352
1386
  const wrap = document.createElement("div");
1353
1387
  wrap.className = "msg-user-wrap";
1354
1388
  wrap.appendChild(el);
@@ -1660,8 +1694,8 @@ const Sessions = (() => {
1660
1694
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1661
1695
  const dDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
1662
1696
  const diffDays = Math.round((today - dDay) / 86400000);
1663
- if (diffDays === 0) return `Today ${hhmm}`;
1664
- if (diffDays === 1) return `Yesterday ${hhmm}`;
1697
+ if (diffDays === 0) return `${I18n.t("sessions.today")} ${hhmm}`;
1698
+ if (diffDays === 1) return `${I18n.t("sessions.yesterday")} ${hhmm}`;
1665
1699
  return `${d.getMonth() + 1}/${d.getDate()} ${hhmm}`;
1666
1700
  }
1667
1701
 
@@ -1748,7 +1782,9 @@ const Sessions = (() => {
1748
1782
  });
1749
1783
 
1750
1784
  bar.appendChild(copyBtn);
1751
- bar.appendChild(editBtn);
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);
1752
1788
  wrap.appendChild(bar);
1753
1789
  }
1754
1790
 
@@ -1802,8 +1838,9 @@ const Sessions = (() => {
1802
1838
  sendBtn.textContent = I18n.t("chat.send");
1803
1839
  sendBtn.addEventListener("click", () => _submitEdit(el, textarea.value.trim()));
1804
1840
 
1841
+ const editIme = IME.track(textarea);
1805
1842
  textarea.addEventListener("keydown", (e) => {
1806
- if (e.key === "Enter" && !e.shiftKey) {
1843
+ if (e.key === "Enter" && !e.shiftKey && !editIme.isComposing(e)) {
1807
1844
  e.preventDefault();
1808
1845
  _submitEdit(el, textarea.value.trim());
1809
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
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.11
4
+ version: 1.4.1
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