openclacky 1.5.4 → 1.5.6

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -0
  3. data/lib/clacky/agent/goal_manager.rb +305 -0
  4. data/lib/clacky/agent/goal_state.rb +88 -0
  5. data/lib/clacky/agent/message_compressor_helper.rb +4 -1
  6. data/lib/clacky/agent/session_serializer.rb +27 -3
  7. data/lib/clacky/agent.rb +202 -3
  8. data/lib/clacky/agent_config.rb +4 -2
  9. data/lib/clacky/agent_profile.rb +25 -3
  10. data/lib/clacky/cli.rb +15 -15
  11. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +29 -1
  12. data/lib/clacky/default_extensions/ext-studio/ext.yml +4 -4
  13. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +130 -59
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +12 -4
  15. data/lib/clacky/default_extensions/git/ext.yml +3 -0
  16. data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
  17. data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
  18. data/lib/clacky/default_searchers/tavily.rb +98 -0
  19. data/lib/clacky/default_skills/new/SKILL.md +17 -12
  20. data/lib/clacky/extension/api_extension.rb +29 -1
  21. data/lib/clacky/extension/loader.rb +1 -0
  22. data/lib/clacky/extension/packager.rb +114 -20
  23. data/lib/clacky/extension/verifier.rb +1 -1
  24. data/lib/clacky/idle_compression_timer.rb +1 -3
  25. data/lib/clacky/message_format/anthropic.rb +1 -1
  26. data/lib/clacky/message_format/bedrock.rb +1 -1
  27. data/lib/clacky/message_format/open_ai.rb +8 -0
  28. data/lib/clacky/providers.rb +65 -8
  29. data/lib/clacky/search_config.rb +57 -0
  30. data/lib/clacky/server/http_server.rb +249 -31
  31. data/lib/clacky/server/session_registry.rb +10 -3
  32. data/lib/clacky/server/web_ui_controller.rb +12 -2
  33. data/lib/clacky/session_manager.rb +1 -0
  34. data/lib/clacky/tools/terminal.rb +30 -3
  35. data/lib/clacky/tools/web_search.rb +45 -6
  36. data/lib/clacky/ui2/components/command_suggestions.rb +17 -0
  37. data/lib/clacky/ui2/components/input_area.rb +26 -2
  38. data/lib/clacky/ui2/ui_controller.rb +17 -0
  39. data/lib/clacky/ui_interface.rb +9 -0
  40. data/lib/clacky/utils/gitignore_parser.rb +7 -1
  41. data/lib/clacky/utils/model_pricing.rb +0 -10
  42. data/lib/clacky/utils/searcher_manager.rb +87 -0
  43. data/lib/clacky/version.rb +1 -1
  44. data/lib/clacky/web/app.css +530 -197
  45. data/lib/clacky/web/app.js +28 -3
  46. data/lib/clacky/web/components/chat-navigator.js +260 -0
  47. data/lib/clacky/web/features/billing/view.js +2 -2
  48. data/lib/clacky/web/features/brand/view.js +5 -1
  49. data/lib/clacky/web/features/extensions/store.js +214 -9
  50. data/lib/clacky/web/features/extensions/view.js +158 -13
  51. data/lib/clacky/web/features/new-session/store.js +25 -3
  52. data/lib/clacky/web/features/new-session/view.js +337 -140
  53. data/lib/clacky/web/features/skills/store.js +4 -2
  54. data/lib/clacky/web/features/skills/view.js +1 -1
  55. data/lib/clacky/web/i18n.js +72 -18
  56. data/lib/clacky/web/index.html +71 -64
  57. data/lib/clacky/web/projects.js +36 -19
  58. data/lib/clacky/web/sessions.js +173 -8
  59. data/lib/clacky/web/settings.js +85 -0
  60. data/lib/clacky/web/skills.js +24 -4
  61. data/lib/clacky/web/utils.js +1 -0
  62. data/lib/clacky/web/ws-dispatcher.js +63 -3
  63. data/lib/clacky.rb +2 -0
  64. metadata +7 -1
@@ -65,6 +65,10 @@ const PANELS = [
65
65
  const Router = (() => {
66
66
  let _current = null; // current view name
67
67
  let _params = {}; // current params (e.g. { id: "abc" } for session view)
68
+ // Teardown function returned by the current ext-workspace render (if any).
69
+ // Invoked when leaving the workspace so the extension's WS subscriptions /
70
+ // timers don't leak across re-entries. See the render contract in ext.js.
71
+ let _wsTeardown = null;
68
72
  let _skipNextHashChange = false; // prevent echo loop when we set hash ourselves
69
73
 
70
74
  // Hide all panels.
@@ -154,6 +158,12 @@ const Router = (() => {
154
158
  _mobileCloseSidebar();
155
159
 
156
160
  // ── Clean up previous state ──────────────────────────────────────────
161
+ // Leaving the current view — if it was an ext-workspace, run its teardown
162
+ // so the extension's WS subscriptions / timers don't leak across re-entries.
163
+ if (typeof _wsTeardown === "function") {
164
+ try { _wsTeardown(); } catch (_e) { /* extension teardown errors are non-fatal */ }
165
+ _wsTeardown = null;
166
+ }
157
167
  if (Sessions.activeId) {
158
168
  Sessions._cacheActiveAndDeselect();
159
169
  }
@@ -316,7 +326,7 @@ const Router = (() => {
316
326
  const container = $("ext-slot-main-workspace");
317
327
  container.replaceChildren();
318
328
  container.style.display = "block";
319
- try { ws.render(container, {}); }
329
+ try { _wsTeardown = ws.render(container, {}); }
320
330
  catch (err) { console.error("ext workspace render failed:", err); }
321
331
  Sessions.renderList();
322
332
  break;
@@ -662,16 +672,23 @@ function showConfirmModal(confId, message) {
662
672
 
663
673
  // ── DOM event listeners ───────────────────────────────────────────────────
664
674
  // Sidebar toggle (with mobile overlay support)
675
+ // Persist the sidebar's visibility so a hard refresh keeps the user's last
676
+ // state instead of always snapping back to "open". (Matches the treatment
677
+ // already given to sidebar *width*, the aside panel, and the todo panel.)
678
+ const _SIDEBAR_HIDDEN_KEY = "clacky-sidebar-hidden";
679
+
665
680
  function _isMobile() { return window.innerWidth <= 768; }
666
681
 
667
682
  function _closeSidebar() {
668
683
  $("sidebar").classList.add("hidden");
669
684
  $("sidebar-overlay").classList.remove("active");
685
+ try { localStorage.setItem(_SIDEBAR_HIDDEN_KEY, "1"); } catch (_e) { /* ignore */ }
670
686
  }
671
687
 
672
688
  function _openSidebar() {
673
689
  $("sidebar").classList.remove("hidden");
674
690
  if (_isMobile()) $("sidebar-overlay").classList.add("active");
691
+ try { localStorage.setItem(_SIDEBAR_HIDDEN_KEY, "0"); } catch (_e) { /* ignore */ }
675
692
  }
676
693
 
677
694
  function _toggleSidebar() {
@@ -737,8 +754,15 @@ $("sidebar-overlay").addEventListener("click", _closeSidebar);
737
754
  });
738
755
  })();
739
756
 
740
- // On mobile: start with sidebar hidden
741
- if (_isMobile()) _closeSidebar();
757
+ // On mobile: start with sidebar hidden.
758
+ // On desktop: restore the user's last sidebar visibility (persisted).
759
+ if (_isMobile()) {
760
+ _closeSidebar();
761
+ } else {
762
+ try {
763
+ if (localStorage.getItem(_SIDEBAR_HIDDEN_KEY) === "1") _closeSidebar();
764
+ } catch (_e) { /* ignore */ }
765
+ }
742
766
 
743
767
  // On mobile: auto-close sidebar when switching sessions/pages
744
768
  function _mobileCloseSidebar() {
@@ -771,6 +795,7 @@ Sidebar.init();
771
795
  Settings.init();
772
796
  Channels.init();
773
797
  Sessions.init();
798
+ ChatNavigator.init();
774
799
 
775
800
  // Boot sequence:
776
801
  // 1. Brand check — shows a dismissible top banner if license activation is needed.
@@ -0,0 +1,260 @@
1
+ // ── Chat Navigator - vertical minimap on the right edge of the chat ────────
2
+ //
3
+ // Shows one bar per user message. Hovering a bar opens a scrollable popup
4
+ // listing every user message preview; clicking a bar or popup item scrolls
5
+ // the chat to that message.
6
+ //
7
+ // The navigator element is absolutely positioned inside #chat-main and its
8
+ // vertical bounds are synced to #messages via ResizeObserver so it never
9
+ // overlaps the composer / info bar below.
10
+ //
11
+ // Depends on: global $ helper (app.js), I18n.
12
+ // ───────────────────────────────────────────────────────────────────────────
13
+ "use strict";
14
+
15
+ (() => {
16
+ const $ = (id) => document.getElementById(id);
17
+
18
+ const NAV_SELECTOR = ".msg-user-wrap";
19
+
20
+ let nav, track, popup, popupList;
21
+ let messages, chatMain;
22
+ let items = []; // [{ el, bar, popupItem, type, preview }]
23
+ let activeIdx = -1;
24
+ let mo, ro;
25
+ let rebuildTimer = null;
26
+ let scrollTimer = null;
27
+ let hideTimer = null;
28
+ let hoveredIdx = -1;
29
+
30
+ // ── Preview text extraction ────────────────────────────────────────────
31
+
32
+ function _previewFor(el) {
33
+ const bubble = el.querySelector(".msg-user");
34
+ if (!bubble) return "";
35
+ const clone = bubble.cloneNode(true);
36
+ clone
37
+ .querySelectorAll("img, .msg-pdf-badge, .msg-user-action-bar, .msg-time")
38
+ .forEach((e) => e.remove());
39
+ const text = clone.textContent.trim();
40
+ return text || (I18n.t("chat.nav.empty") || "(empty)");
41
+ }
42
+
43
+ // ── Position sync ───────────────────────────────────────────────────────
44
+
45
+ function _syncBounds() {
46
+ if (!nav || !messages || !chatMain) return;
47
+ const msgRect = messages.getBoundingClientRect();
48
+ const mainRect = chatMain.getBoundingClientRect();
49
+ nav.style.top = msgRect.top - mainRect.top + "px";
50
+ nav.style.height = msgRect.height + "px";
51
+ }
52
+
53
+ // ── Rebuild bars + popup list ───────────────────────────────────────────
54
+
55
+ function _scheduleRebuild() {
56
+ clearTimeout(rebuildTimer);
57
+ rebuildTimer = setTimeout(_rebuild, 120);
58
+ }
59
+
60
+ function _rebuild() {
61
+ if (!messages || !track) return;
62
+ const els = Array.from(messages.querySelectorAll(NAV_SELECTOR));
63
+ items = els.map((el) => ({
64
+ el,
65
+ preview: _previewFor(el),
66
+ }));
67
+
68
+ // Hide navigator when content fits without scrolling
69
+ const scrollable = messages.scrollHeight > messages.clientHeight + 40;
70
+ nav.style.display = scrollable && items.length > 1 ? "" : "none";
71
+ if (!scrollable || items.length <= 1) return;
72
+
73
+ // Build bars
74
+ track.innerHTML = "";
75
+ items.forEach((item, i) => {
76
+ const bar = document.createElement("div");
77
+ bar.className = "chat-nav-bar";
78
+ bar.dataset.idx = String(i);
79
+ bar.title = item.preview.slice(0, 120);
80
+ bar.addEventListener("mouseenter", () => _onBarHover(i));
81
+ bar.addEventListener("click", () => _scrollToIdx(i));
82
+ track.appendChild(bar);
83
+ item.bar = bar;
84
+ });
85
+
86
+ // Build popup list (lazy: build once per rebuild)
87
+ popupList.innerHTML = "";
88
+ items.forEach((item, i) => {
89
+ const li = document.createElement("div");
90
+ li.className = "chat-nav-popup-item";
91
+ li.dataset.idx = String(i);
92
+ li.innerHTML =
93
+ `<span class="chat-nav-popup-item-text">${escapeHtml(item.preview)}</span>`;
94
+ li.addEventListener("mouseenter", () => _onItemHover(i));
95
+ li.addEventListener("click", () => _scrollToIdx(i));
96
+ popupList.appendChild(li);
97
+ item.popupItem = li;
98
+ });
99
+
100
+ activeIdx = -1;
101
+ _updateActive();
102
+ }
103
+
104
+ // ── Active bar tracking ─────────────────────────────────────────────────
105
+
106
+ function _updateActive() {
107
+ if (!items.length) return;
108
+ const threshold = messages.getBoundingClientRect().top + 30;
109
+ let idx = 0;
110
+ for (let i = 0; i < items.length; i++) {
111
+ if (!items[i].el || !items[i].el.isConnected) {
112
+ _scheduleRebuild();
113
+ return;
114
+ }
115
+ const top = items[i].el.getBoundingClientRect().top;
116
+ if (top <= threshold) idx = i;
117
+ else break;
118
+ }
119
+ if (idx !== activeIdx) {
120
+ activeIdx = idx;
121
+ items.forEach((item, i) => {
122
+ item.bar.classList.toggle("active", i === idx);
123
+ });
124
+ }
125
+ }
126
+
127
+ function _onScroll() {
128
+ clearTimeout(scrollTimer);
129
+ scrollTimer = setTimeout(_updateActive, 80);
130
+ }
131
+
132
+ // ── Popup interaction ───────────────────────────────────────────────────
133
+
134
+ function _onBarHover(idx) {
135
+ clearTimeout(hideTimer);
136
+ _showPopup(idx);
137
+ }
138
+
139
+ function _onItemHover(idx) {
140
+ clearTimeout(hideTimer);
141
+ hoveredIdx = idx;
142
+ items.forEach((item, i) => {
143
+ item.bar.classList.toggle("hovered", i === idx);
144
+ });
145
+ }
146
+
147
+ function _showPopup(idx) {
148
+ if (!popup || !items.length) return;
149
+ hoveredIdx = idx;
150
+ popup.style.display = "flex";
151
+
152
+ items.forEach((item, i) => {
153
+ item.popupItem.classList.toggle("highlighted", i === idx);
154
+ item.bar.classList.toggle("hovered", i === idx);
155
+ });
156
+
157
+ // Position popup vertically centered on the hovered bar
158
+ const bar = items[idx].bar;
159
+ const navRect = nav.getBoundingClientRect();
160
+ const barRect = bar.getBoundingClientRect();
161
+ const barCenter = barRect.top + barRect.height / 2 - navRect.top;
162
+ popup.style.top = "";
163
+ popup.style.bottom = "";
164
+ const popupHeight = popup.offsetHeight;
165
+ let top = barCenter - popupHeight / 2;
166
+ top = Math.max(0, Math.min(top, navRect.height - popupHeight));
167
+ popup.style.top = top + "px";
168
+
169
+ const target = items[idx].popupItem;
170
+ if (target) {
171
+ const listRect = popupList.getBoundingClientRect();
172
+ const itemRect = target.getBoundingClientRect();
173
+ const offset =
174
+ itemRect.top - listRect.top - (listRect.height - itemRect.height) / 2;
175
+ popupList.scrollTop += offset;
176
+ }
177
+ }
178
+
179
+ function _hidePopup() {
180
+ clearTimeout(hideTimer);
181
+ hideTimer = setTimeout(() => {
182
+ if (!popup) return;
183
+ popup.style.display = "none";
184
+ hoveredIdx = -1;
185
+ items.forEach((item) => {
186
+ item.bar.classList.remove("hovered");
187
+ item.popupItem.classList.remove("highlighted");
188
+ });
189
+ }, 200);
190
+ }
191
+
192
+ // ── Scroll to message ───────────────────────────────────────────────────
193
+
194
+ function _scrollToIdx(idx) {
195
+ if (!items[idx] || !items[idx].el) return;
196
+ const el = items[idx].el;
197
+ const msgRect = messages.getBoundingClientRect();
198
+ const elRect = el.getBoundingClientRect();
199
+ const offset = elRect.top - msgRect.top;
200
+ messages.scrollTop += offset - 12;
201
+ _hidePopup();
202
+ }
203
+
204
+ // ── Init ────────────────────────────────────────────────────────────────
205
+
206
+ function init() {
207
+ messages = $("messages");
208
+ chatMain = $("chat-main");
209
+ if (!messages || !chatMain) return;
210
+
211
+ nav = document.createElement("div");
212
+ nav.className = "chat-navigator";
213
+ nav.style.display = "none";
214
+ nav.innerHTML =
215
+ '<div class="chat-nav-track"></div>' +
216
+ '<div class="chat-nav-popup" style="display:none">' +
217
+ '<div class="chat-nav-popup-header">' +
218
+ '<span class="chat-nav-popup-title"></span>' +
219
+ "</div>" +
220
+ '<div class="chat-nav-popup-list"></div>' +
221
+ "</div>";
222
+ chatMain.appendChild(nav);
223
+ track = nav.querySelector(".chat-nav-track");
224
+ popup = nav.querySelector(".chat-nav-popup");
225
+ popupList = nav.querySelector(".chat-nav-popup-list");
226
+
227
+ const titleEl = nav.querySelector(".chat-nav-popup-title");
228
+ if (titleEl) titleEl.textContent = I18n.t("chat.nav.title") || "Messages";
229
+
230
+ // Hover open / leave close
231
+ nav.addEventListener("mouseenter", () => clearTimeout(hideTimer));
232
+ nav.addEventListener("mouseleave", _hidePopup);
233
+
234
+ // Observe message children changes (add / remove / clear)
235
+ mo = new MutationObserver(() => _scheduleRebuild());
236
+ mo.observe(messages, { childList: true });
237
+
238
+ // Sync navigator bounds when layout changes
239
+ ro = new ResizeObserver(() => {
240
+ _syncBounds();
241
+ _scheduleRebuild();
242
+ });
243
+ ro.observe(chatMain);
244
+ ro.observe(messages);
245
+
246
+ // Track scroll position for active bar
247
+ messages.addEventListener("scroll", _onScroll, { passive: true });
248
+
249
+ // Re-render title on language change
250
+ document.addEventListener("langchange", () => {
251
+ const t = nav.querySelector(".chat-nav-popup-title");
252
+ if (t) t.textContent = I18n.t("chat.nav.title") || "Messages";
253
+ });
254
+
255
+ _syncBounds();
256
+ _scheduleRebuild();
257
+ }
258
+
259
+ window.ChatNavigator = { init };
260
+ })();
@@ -819,13 +819,13 @@ const BillingView = (() => {
819
819
  <div class="billing-cell billing-cell-number billing-cell-total">${_formatCompact(totalTokens)}</div>
820
820
  <div class="billing-cell billing-cell-number billing-cell-hit">${_formatCompact(cacheHit)}</div>
821
821
  <div class="billing-cell billing-cell-number billing-cell-miss">${_formatCompact(cacheMiss)}</div>
822
- <div class="billing-cell billing-cell-number">${_formatCompact(completionTokens)}</div>
822
+ <div class="billing-cell billing-cell-number billing-cell-output">${_formatCompact(completionTokens)}</div>
823
823
  <div class="billing-cell billing-cell-cost">${Billing.getCurrencySymbol()}${_formatCost(totalCost)}</div>
824
824
  <div class="billing-cell billing-cell-time">${lastRequest}</div>
825
825
  <div class="billing-session-numbers-row">
826
826
  <span class="billing-cell-number">${_formatCompact(totalTokens)} tok</span>
827
827
  <span class="billing-cell-number billing-cell-hit">${_formatCompact(cacheHit)} hit</span>
828
- <span class="billing-cell-number">${_formatCompact(completionTokens)} out</span>
828
+ <span class="billing-cell-number billing-cell-output">${_formatCompact(completionTokens)} out</span>
829
829
  <span class="billing-cell-cost">${Billing.getCurrencySymbol()}${_formatCost(totalCost)}</span>
830
830
  <span class="billing-cell-time">${lastRequest}</span>
831
831
  </div>
@@ -237,7 +237,11 @@ const BrandView = (() => {
237
237
  const hasLogo = !!(info.logo_url && logoImg);
238
238
 
239
239
  if (hasLogo) {
240
- if (logoImg.src && logoImg.src === info.logo_url) {
240
+ const normalizedSrc = logoImg.src ? logoImg.src.replace(/\/$/, "") : "";
241
+ const normalizedUrl = info.logo_url.replace(/\/$/, "");
242
+ if (normalizedSrc === normalizedUrl) {
243
+ logoImg.style.display = "";
244
+ if (brandWrap) brandWrap.classList.add("has-logo");
241
245
  _applyFavicon(info.logo_url);
242
246
  } else {
243
247
  const img = new Image();