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.
@@ -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 = "newest"; // "newest" | "updated" | "downloads"
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 || "newest";
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
- ${_renderEntryPoints(ext.contributes)}
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 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
 
@@ -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": "+ New Session",
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": "面板",
@@ -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
- <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>
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>
@@ -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) => {
@@ -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
- 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);
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
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.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy