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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/lib/clacky/agent/goal_manager.rb +305 -0
- data/lib/clacky/agent/goal_state.rb +88 -0
- data/lib/clacky/agent/message_compressor_helper.rb +4 -1
- data/lib/clacky/agent/session_serializer.rb +27 -3
- data/lib/clacky/agent.rb +202 -3
- data/lib/clacky/agent_config.rb +4 -2
- data/lib/clacky/agent_profile.rb +25 -3
- data/lib/clacky/cli.rb +15 -15
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +29 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +4 -4
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +130 -59
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +12 -4
- data/lib/clacky/default_extensions/git/ext.yml +3 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
- data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
- data/lib/clacky/default_searchers/tavily.rb +98 -0
- data/lib/clacky/default_skills/new/SKILL.md +17 -12
- data/lib/clacky/extension/api_extension.rb +29 -1
- data/lib/clacky/extension/loader.rb +1 -0
- data/lib/clacky/extension/packager.rb +114 -20
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/idle_compression_timer.rb +1 -3
- data/lib/clacky/message_format/anthropic.rb +1 -1
- data/lib/clacky/message_format/bedrock.rb +1 -1
- data/lib/clacky/message_format/open_ai.rb +8 -0
- data/lib/clacky/providers.rb +65 -8
- data/lib/clacky/search_config.rb +57 -0
- data/lib/clacky/server/http_server.rb +249 -31
- data/lib/clacky/server/session_registry.rb +10 -3
- data/lib/clacky/server/web_ui_controller.rb +12 -2
- data/lib/clacky/session_manager.rb +1 -0
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/tools/web_search.rb +45 -6
- data/lib/clacky/ui2/components/command_suggestions.rb +17 -0
- data/lib/clacky/ui2/components/input_area.rb +26 -2
- data/lib/clacky/ui2/ui_controller.rb +17 -0
- data/lib/clacky/ui_interface.rb +9 -0
- data/lib/clacky/utils/gitignore_parser.rb +7 -1
- data/lib/clacky/utils/model_pricing.rb +0 -10
- data/lib/clacky/utils/searcher_manager.rb +87 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +530 -197
- data/lib/clacky/web/app.js +28 -3
- data/lib/clacky/web/components/chat-navigator.js +260 -0
- data/lib/clacky/web/features/billing/view.js +2 -2
- data/lib/clacky/web/features/brand/view.js +5 -1
- data/lib/clacky/web/features/extensions/store.js +214 -9
- data/lib/clacky/web/features/extensions/view.js +158 -13
- data/lib/clacky/web/features/new-session/store.js +25 -3
- data/lib/clacky/web/features/new-session/view.js +337 -140
- data/lib/clacky/web/features/skills/store.js +4 -2
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/i18n.js +72 -18
- data/lib/clacky/web/index.html +71 -64
- data/lib/clacky/web/projects.js +36 -19
- data/lib/clacky/web/sessions.js +173 -8
- data/lib/clacky/web/settings.js +85 -0
- data/lib/clacky/web/skills.js +24 -4
- data/lib/clacky/web/utils.js +1 -0
- data/lib/clacky/web/ws-dispatcher.js +63 -3
- data/lib/clacky.rb +2 -0
- metadata +7 -1
data/lib/clacky/web/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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();
|