clay-server 2.32.0-beta.7 → 2.32.0-beta.9

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.
@@ -9,6 +9,7 @@ import { getWs } from './ws-ref.js';
9
9
  import { openDm } from './app-dm.js';
10
10
  import { getCachedProjects } from './app-projects.js';
11
11
  import { switchProject } from './app-projects.js';
12
+ import { mateAvatarUrl } from './avatar.js';
12
13
  var notifications = [];
13
14
  var unreadCount = 0;
14
15
  var bannerContainer = null;
@@ -16,10 +17,10 @@ var bellBtn = null;
16
17
  var badgeEl = null;
17
18
 
18
19
  // --- Update available banner state ---
20
+ // Server pushes update_available on an hourly boundary; dismissal is
21
+ // per-banner-instance and doesn't need to persist. The next server push
22
+ // (next hour) acts as a fresh ping.
19
23
  var pendingUpdateMsg = null;
20
- var updateReshowTimer = null;
21
- var updateDismissedAt = 0;
22
- var UPDATE_RESHOW_INTERVAL = 60 * 60 * 1000; // 1 hour
23
24
 
24
25
  // ========================================================
25
26
  // Init
@@ -50,10 +51,12 @@ function showAllBanners() {
50
51
  // Clear existing banners first
51
52
  if (bannerContainer) bannerContainer.innerHTML = "";
52
53
 
53
- // Re-add update banner if present
54
+ // Re-add update banner if present (may be suppressed by recent dismiss)
54
55
  if (pendingUpdateMsg) showUpdateBanner(pendingUpdateMsg);
55
56
 
56
- if (notifications.length === 0 && !pendingUpdateMsg) {
57
+ // Check if any banner actually got rendered (update banner can be suppressed)
58
+ var hasVisibleBanner = bannerContainer.children.length > 0;
59
+ if (notifications.length === 0 && !hasVisibleBanner) {
57
60
  showBanner({
58
61
  id: "_empty",
59
62
  type: "info",
@@ -80,14 +83,17 @@ function showBanner(notif, autoDismissMs) {
80
83
  var projectIcon = isEmpty ? null : getProjectIcon(notif.slug);
81
84
  var projectName = isEmpty ? "" : getProjectName(notif.slug);
82
85
  var isPermission = notif.type === "permission_request" && notif.meta && notif.meta.requestId;
86
+ var mate = isEmpty ? null : getMateForNotification(notif);
83
87
 
84
88
  var banner = document.createElement("div");
85
89
  banner.className = "notif-banner" + (isPermission ? " notif-banner-permission" : "");
86
90
  if (!isEmpty) banner.setAttribute("data-notif-id", notif.id);
87
91
 
88
- var iconHtmlStr = projectIcon
89
- ? '<span class="notif-banner-emoji">' + projectIcon + '</span>'
90
- : iconHtml(isEmpty ? "check-circle" : "folder");
92
+ var iconHtmlStr = mate
93
+ ? '<img class="notif-banner-avatar" src="' + escapeHtml(mateAvatarUrl(mate, 32)) + '" alt="' + escapeHtml(mate.displayName || mate.name || "Mate") + '">'
94
+ : projectIcon
95
+ ? '<span class="notif-banner-emoji">' + projectIcon + '</span>'
96
+ : iconHtml(isEmpty ? "check-circle" : "folder");
91
97
 
92
98
  // Format permission title as "Can I ..." style
93
99
  if (isPermission && notif.meta) {
@@ -102,7 +108,7 @@ function showBanner(notif, autoDismissMs) {
102
108
  actionsHtml =
103
109
  '<div class="notif-banner-actions">' +
104
110
  '<button class="notif-banner-allow">Sure</button>' +
105
- '<button class="notif-banner-always">Always allow</button>' +
111
+ '<button class="notif-banner-always">Allow for session</button>' +
106
112
  '<button class="notif-banner-deny">No</button>' +
107
113
  '<button class="notif-banner-goto" title="Go to session">' + iconHtml("external-link") + '</button>' +
108
114
  '</div>';
@@ -301,8 +307,12 @@ function updateBadge() {
301
307
  // ========================================================
302
308
 
303
309
  function navigateToNotification(notif) {
304
- if (notif.mateId) {
305
- openDm(notif.mateId);
310
+ var mateId = notif.mateId || deriveMateIdFromNotification(notif);
311
+ if (mateId) {
312
+ if (notif.sessionId) {
313
+ try { sessionStorage.setItem("pending-notif-session", notif.sessionId); } catch (e) {}
314
+ }
315
+ openDm(mateId);
306
316
  return;
307
317
  }
308
318
 
@@ -323,6 +333,25 @@ function navigateToNotification(notif) {
323
333
  }
324
334
  }
325
335
 
336
+ function deriveMateIdFromNotification(notif) {
337
+ if (!notif) return null;
338
+ if (typeof notif.slug === "string" && notif.slug.indexOf("mate-") === 0) {
339
+ return notif.slug.substring(5) || null;
340
+ }
341
+ return null;
342
+ }
343
+
344
+ function getMateForNotification(notif) {
345
+ var mateId = notif && notif.meta ? notif.meta.avatarMateId : null;
346
+ if (!mateId) mateId = deriveMateIdFromNotification(notif);
347
+ if (!mateId) return null;
348
+ var mates = store.get('cachedMatesList') || [];
349
+ for (var i = 0; i < mates.length; i++) {
350
+ if (mates[i] && mates[i].id === mateId) return mates[i];
351
+ }
352
+ return { id: mateId };
353
+ }
354
+
326
355
  // ========================================================
327
356
  // Update available banner
328
357
  // ========================================================
@@ -332,9 +361,6 @@ export function showUpdateBanner(msg) {
332
361
  pendingUpdateMsg = msg;
333
362
  if (!bannerContainer) return;
334
363
 
335
- // If user dismissed recently, skip until reshow timer fires
336
- if (updateDismissedAt && (Date.now() - updateDismissedAt) < UPDATE_RESHOW_INTERVAL) return;
337
-
338
364
  // Remove any existing update banner
339
365
  var existing = bannerContainer.querySelector('[data-notif-id="_update"]');
340
366
  if (existing) removeBanner(existing);
@@ -387,27 +413,17 @@ export function showUpdateBanner(msg) {
387
413
  });
388
414
  }
389
415
 
390
- // Close button -> dismiss, re-show in 1 hour
416
+ // Close button -> dismiss. No local throttle; the server pushes a new
417
+ // update_available on the next hour boundary, which re-shows naturally.
391
418
  var closeBtn = banner.querySelector(".notif-banner-close");
392
419
  if (closeBtn) {
393
420
  closeBtn.addEventListener("click", function (e) {
394
421
  e.stopPropagation();
395
422
  removeBanner(banner);
396
- scheduleUpdateReshow();
397
423
  });
398
424
  }
399
425
  }
400
426
 
401
- function scheduleUpdateReshow() {
402
- updateDismissedAt = Date.now();
403
- if (updateReshowTimer) clearTimeout(updateReshowTimer);
404
- updateReshowTimer = setTimeout(function () {
405
- updateReshowTimer = null;
406
- updateDismissedAt = 0;
407
- if (pendingUpdateMsg) showUpdateBanner(pendingUpdateMsg);
408
- }, UPDATE_RESHOW_INTERVAL);
409
- }
410
-
411
427
  // ========================================================
412
428
  // Helpers
413
429
  // ========================================================
@@ -335,15 +335,15 @@ function rebuildCodexSections() {
335
335
 
336
336
  if (configApprovalSection) {
337
337
  configApprovalSection.style.display = isCodex ? "" : "none";
338
- if (isCodex) buildSegmentedBar(configApprovalBar, CODEX_APPROVAL_OPTIONS, s.codexApproval || "on-failure", "set_codex_approval", "approval");
338
+ if (isCodex) buildSegmentedBar(configApprovalBar, CODEX_APPROVAL_OPTIONS, s.codexApproval, "set_codex_approval", "approval");
339
339
  }
340
340
  if (configSandboxSection) {
341
341
  configSandboxSection.style.display = isCodex ? "" : "none";
342
- if (isCodex) buildSegmentedBar(configSandboxBar, CODEX_SANDBOX_OPTIONS, s.codexSandbox || "workspace-write", "set_codex_sandbox", "sandbox");
342
+ if (isCodex) buildSegmentedBar(configSandboxBar, CODEX_SANDBOX_OPTIONS, s.codexSandbox, "set_codex_sandbox", "sandbox");
343
343
  }
344
344
  if (configWebsearchSection) {
345
345
  configWebsearchSection.style.display = isCodex ? "" : "none";
346
- if (isCodex) buildSegmentedBar(configWebsearchBar, CODEX_WEBSEARCH_OPTIONS, s.codexWebSearch || "disabled", "set_codex_websearch", "webSearch");
346
+ if (isCodex) buildSegmentedBar(configWebsearchBar, CODEX_WEBSEARCH_OPTIONS, s.codexWebSearch, "set_codex_websearch", "webSearch");
347
347
  }
348
348
  }
349
349
 
@@ -17,6 +17,8 @@ export function initContextSources(_ctx) {
17
17
 
18
18
  var addBtn = document.getElementById("context-sources-add");
19
19
  var picker = document.getElementById("context-sources-picker");
20
+ // Suppress tooltip when the picker is open
21
+ if (addBtn) addBtn.setAttribute("data-tip-suppress-when-open", "#context-sources-picker");
20
22
 
21
23
  addBtn.addEventListener("click", function(e) {
22
24
  e.stopPropagation();
@@ -36,8 +38,22 @@ export function initContextSources(_ctx) {
36
38
 
37
39
  function closePicker() {
38
40
  var picker = document.getElementById("context-sources-picker");
39
- picker.classList.add("hidden");
41
+ if (picker) picker.classList.add("hidden");
40
42
  document.removeEventListener("click", closePicker, true);
43
+ // Also close mobile bottom sheet if open
44
+ var moreSheet = document.getElementById("input-more-sheet");
45
+ if (moreSheet && moreSheet.classList.contains("open")) {
46
+ moreSheet.classList.remove("open");
47
+ setTimeout(function () { moreSheet.classList.add("hidden"); }, 250);
48
+ }
49
+ }
50
+
51
+ // Re-render all open picker surfaces (desktop popover and mobile bottom sheet)
52
+ function renderAllOpen() {
53
+ var picker = document.getElementById("context-sources-picker");
54
+ if (picker && !picker.classList.contains("hidden")) renderPicker();
55
+ var moreSheet = document.getElementById("input-more-sheet");
56
+ if (moreSheet && moreSheet.classList.contains("open")) renderPicker("-mobile");
41
57
  }
42
58
 
43
59
  // Restore state from server
@@ -80,11 +96,7 @@ export function updateTerminalList(terminals) {
80
96
  if (changed) saveToServer();
81
97
  renderChips();
82
98
 
83
- // If picker is open, re-render it
84
- var picker = document.getElementById("context-sources-picker");
85
- if (!picker.classList.contains("hidden")) {
86
- renderPicker();
87
- }
99
+ renderAllOpen();
88
100
  }
89
101
 
90
102
  // Called when Chrome extension sends tab list via postMessage
@@ -110,11 +122,7 @@ export function updateBrowserTabList(tabs) {
110
122
  if (changed) saveToServer();
111
123
  renderChips();
112
124
 
113
- // If picker is open, re-render it
114
- var picker = document.getElementById("context-sources-picker");
115
- if (!picker.classList.contains("hidden")) {
116
- renderPicker();
117
- }
125
+ renderAllOpen();
118
126
  }
119
127
 
120
128
  // Called when email_accounts_list arrives from server
@@ -141,10 +149,7 @@ export function updateEmailAccountList(msg) {
141
149
  if (changed) saveToServer();
142
150
  renderChips();
143
151
 
144
- var picker = document.getElementById("context-sources-picker");
145
- if (!picker.classList.contains("hidden")) {
146
- renderPicker();
147
- }
152
+ renderAllOpen();
148
153
  }
149
154
 
150
155
  // Called when email_unread_update arrives from server
@@ -152,10 +157,7 @@ export function updateEmailUnreadCounts(msg) {
152
157
  emailUnreadCounts = msg.unread || {};
153
158
  renderChips();
154
159
 
155
- var picker = document.getElementById("context-sources-picker");
156
- if (!picker.classList.contains("hidden")) {
157
- renderPicker();
158
- }
160
+ renderAllOpen();
159
161
  }
160
162
 
161
163
  function toggleSource(sourceId) {
@@ -166,7 +168,7 @@ function toggleSource(sourceId) {
166
168
  }
167
169
  saveToServer();
168
170
  renderChips();
169
- renderPicker();
171
+ renderAllOpen();
170
172
  }
171
173
 
172
174
  function removeSource(sourceId) {
@@ -174,10 +176,56 @@ function removeSource(sourceId) {
174
176
  saveToServer();
175
177
  renderChips();
176
178
 
177
- var picker = document.getElementById("context-sources-picker");
178
- if (!picker.classList.contains("hidden")) {
179
- renderPicker();
179
+ renderAllOpen();
180
+ }
181
+
182
+ function buildActiveSourceRow(iconHtml, text) {
183
+ return '<div class="ctx-tip-row">' + iconHtml + '<span>' + escapeHtml(text) + '</span></div>';
184
+ }
185
+
186
+ function getActiveSourceRowsHTML() {
187
+ var rows = [];
188
+ for (var id of activeSourceIds) {
189
+ var parts = id.split(":");
190
+ var type = parts[0];
191
+ var key = parts.slice(1).join(":");
192
+ if (type === "term") {
193
+ for (var i = 0; i < terminalList.length; i++) {
194
+ if (String(terminalList[i].id) === key) {
195
+ rows.push(buildActiveSourceRow(
196
+ '<i data-lucide="square-terminal"></i>',
197
+ terminalList[i].title || ("Terminal " + key)
198
+ ));
199
+ break;
200
+ }
201
+ }
202
+ } else if (type === "tab") {
203
+ var tabId = parseInt(key, 10);
204
+ for (var j = 0; j < browserTabList.length; j++) {
205
+ if (browserTabList[j].id === tabId) {
206
+ var t = browserTabList[j];
207
+ var title = t.title || t.url || "Tab";
208
+ if (title.length > 50) title = title.slice(0, 47) + "...";
209
+ var faviconHtml = t.favIconUrl
210
+ ? '<img src="' + escapeHtml(t.favIconUrl) + '" class="ctx-tip-favicon" onerror="this.style.display=\'none\'">'
211
+ : '<i data-lucide="globe"></i>';
212
+ rows.push(buildActiveSourceRow(faviconHtml, title));
213
+ break;
214
+ }
215
+ }
216
+ } else if (type === "email") {
217
+ for (var k = 0; k < emailAccountList.length; k++) {
218
+ if (emailAccountList[k].id === key) {
219
+ rows.push(buildActiveSourceRow(
220
+ '<i data-lucide="mail"></i>',
221
+ emailAccountList[k].email
222
+ ));
223
+ break;
224
+ }
225
+ }
226
+ }
180
227
  }
228
+ return rows;
181
229
  }
182
230
 
183
231
  function renderChips() {
@@ -193,15 +241,30 @@ function renderChips() {
193
241
  addBtn.appendChild(existingBadge);
194
242
  }
195
243
  existingBadge.textContent = activeSourceIds.size;
244
+ var rows = getActiveSourceRowsHTML();
245
+ if (rows.length > 0) {
246
+ var html = '<div class="ctx-tip-header">Active context sources</div>' + rows.join("");
247
+ addBtn.setAttribute("data-tip-html", html);
248
+ addBtn.removeAttribute("data-tip");
249
+ } else {
250
+ addBtn.setAttribute("data-tip", "Add context sources");
251
+ addBtn.removeAttribute("data-tip-html");
252
+ }
253
+ addBtn.removeAttribute("title");
196
254
  } else {
197
255
  if (labelSpan) { labelSpan.style.display = ""; }
198
256
  if (existingBadge) existingBadge.remove();
257
+ addBtn.setAttribute("data-tip", "Add context sources");
258
+ addBtn.removeAttribute("data-tip-html");
259
+ addBtn.removeAttribute("title");
199
260
  }
200
261
  }
201
262
 
202
- function renderPicker() {
263
+ export function renderPicker(suffix) {
264
+ suffix = suffix || "";
203
265
  // --- Terminals section ---
204
- var termSection = document.getElementById("context-picker-terminals");
266
+ var termSection = document.getElementById("context-picker-terminals" + suffix);
267
+ if (!termSection) return;
205
268
  termSection.innerHTML = "";
206
269
 
207
270
  var termLabel = document.createElement("div");
@@ -239,7 +302,8 @@ function renderPicker() {
239
302
  }
240
303
 
241
304
  // --- Browser Tabs section ---
242
- var tabSection = document.getElementById("context-picker-tabs");
305
+ var tabSection = document.getElementById("context-picker-tabs" + suffix);
306
+ if (!tabSection) return;
243
307
  tabSection.innerHTML = "";
244
308
 
245
309
  var tabLabel = document.createElement("div");
@@ -299,7 +363,8 @@ function renderPicker() {
299
363
  }
300
364
 
301
365
  // --- Email Accounts section ---
302
- var emailSection = document.getElementById("context-picker-email");
366
+ var emailSection = document.getElementById("context-picker-email" + suffix);
367
+ if (!emailSection) return;
303
368
  emailSection.innerHTML = "";
304
369
 
305
370
  var emailLabel = document.createElement("div");
@@ -1,5 +1,6 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { setRewindMode, isRewindMode } from './rewind.js';
3
+ import { renderPicker as renderContextPicker } from './context-sources.js';
3
4
  import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible, mentionMenuKeydown, setMentionAtIdx, parseMentionFromInput, clearMentionState, stickyReapplyMention, sendMention, renderMentionUser, removeMentionChip } from './mention.js';
4
5
  import { store } from './store.js';
5
6
  import { mateAvatarUrl } from './avatar.js';
@@ -657,6 +658,41 @@ export function initInput(_ctx) {
657
658
  });
658
659
  }
659
660
 
661
+ // Mobile "+" button -> unified bottom sheet with attach/image + context sources
662
+ var moreBtn = document.getElementById("input-more-btn");
663
+ var moreSheet = document.getElementById("input-more-sheet");
664
+ function openMoreSheet() {
665
+ if (!moreSheet) return;
666
+ // Render context sources into mobile sheet containers
667
+ try { renderContextPicker("-mobile"); } catch (e) {}
668
+ moreSheet.classList.remove("hidden");
669
+ requestAnimationFrame(function () { moreSheet.classList.add("open"); });
670
+ }
671
+ function closeMoreSheet() {
672
+ if (!moreSheet) return;
673
+ moreSheet.classList.remove("open");
674
+ setTimeout(function () { moreSheet.classList.add("hidden"); }, 250);
675
+ }
676
+ if (moreBtn && moreSheet) {
677
+ moreBtn.addEventListener("click", function (e) {
678
+ e.stopPropagation();
679
+ openMoreSheet();
680
+ });
681
+ var backdrop = moreSheet.querySelector(".input-more-backdrop");
682
+ if (backdrop) backdrop.addEventListener("click", closeMoreSheet);
683
+
684
+ var moreAttach = document.getElementById("input-more-attach");
685
+ if (moreAttach) moreAttach.addEventListener("click", function () {
686
+ closeMoreSheet();
687
+ createFileInput(null, null, true);
688
+ });
689
+ var moreImage = document.getElementById("input-more-image");
690
+ if (moreImage) moreImage.addEventListener("click", function () {
691
+ closeMoreSheet();
692
+ createFileInput("image/*", null, true);
693
+ });
694
+ }
695
+
660
696
  // Schedule button — inline expand with minute input
661
697
  var scheduleBtn = document.getElementById("schedule-btn");
662
698
  var scheduleInlineInput = null;
@@ -152,6 +152,18 @@ export function initTerminal(_ctx) {
152
152
  fitTerminal();
153
153
  });
154
154
 
155
+ // Header toggle button
156
+ var toggleBtn = document.getElementById("terminal-toggle-btn");
157
+ if (toggleBtn) {
158
+ toggleBtn.addEventListener("click", function () {
159
+ if (isOpen && !ctx.terminalContainerEl.classList.contains("hidden")) {
160
+ closeTerminal();
161
+ } else {
162
+ openTerminal();
163
+ }
164
+ });
165
+ }
166
+
155
167
  // Sidebar terminal button
156
168
  var sidebarTermBtn = document.getElementById("terminal-sidebar-btn");
157
169
  if (sidebarTermBtn) {
@@ -569,7 +569,7 @@ function renderFormalPermission(requestId, toolName, toolInput, decisionReason)
569
569
 
570
570
  var allowAlwaysBtn = document.createElement("button");
571
571
  allowAlwaysBtn.className = "permission-btn permission-allow-session";
572
- allowAlwaysBtn.textContent = "Always Allow";
572
+ allowAlwaysBtn.textContent = "Allow for Session";
573
573
  allowAlwaysBtn.addEventListener("click", function () {
574
574
  sendPermissionResponse(container, requestId, "allow_always");
575
575
  });
@@ -856,7 +856,7 @@ function renderConversationalPermission(requestId, toolName, toolInput, mateId,
856
856
 
857
857
  var alwaysBtn = document.createElement("button");
858
858
  alwaysBtn.className = "mate-permission-reply mate-permission-always";
859
- alwaysBtn.textContent = "Always allow";
859
+ alwaysBtn.textContent = "Allow for session";
860
860
  alwaysBtn.addEventListener("click", function () {
861
861
  sendPermissionResponse(container, requestId, "allow_always");
862
862
  });
@@ -887,7 +887,7 @@ function sendPermissionResponse(container, requestId, decision) {
887
887
  container.classList.add("resolved");
888
888
  if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
889
889
 
890
- var label = decision === "deny" ? "Denied" : "Allowed";
890
+ var label = decision === "deny" ? "Denied" : decision === "allow_always" ? "Allowed for session" : "Allowed";
891
891
  var resolvedClass = decision === "deny" ? "resolved-denied" : "resolved-allowed";
892
892
  container.classList.add(resolvedClass);
893
893
 
@@ -928,7 +928,7 @@ export function markPermissionResolved(requestId, decision) {
928
928
  var resolvedClass = isDeny ? "resolved-denied" : "resolved-allowed";
929
929
  container.classList.add(resolvedClass);
930
930
 
931
- var label = planLabelMap[decision] || (decision === "deny" ? "Denied" : "Allowed");
931
+ var label = planLabelMap[decision] || (decision === "deny" ? "Denied" : decision === "allow_always" ? "Allowed for session" : "Allowed");
932
932
  var actions = container.querySelector(".permission-actions") || container.querySelector(".plan-permission-actions");
933
933
  if (actions) {
934
934
  actions.innerHTML = '<span class="permission-decision-label">' + label + '</span>';
@@ -6,6 +6,7 @@
6
6
  var tooltipEl = null;
7
7
  var showTimer = null;
8
8
  var SHOW_DELAY = 120;
9
+ var currentTarget = null;
9
10
 
10
11
  function initTooltips() {
11
12
  // Create singleton tooltip element
@@ -18,14 +19,17 @@ function initTooltips() {
18
19
 
19
20
  // Delegate hover events on document for [data-tip]
20
21
  document.addEventListener("mouseover", function (e) {
21
- var target = e.target.closest("[data-tip]");
22
+ var target = e.target.closest("[data-tip], [data-tip-html]");
22
23
  if (!target) return;
24
+ if (target === currentTarget) return; // already showing for this target
23
25
  scheduleShow(target);
24
26
  });
25
27
 
26
28
  document.addEventListener("mouseout", function (e) {
27
- var target = e.target.closest("[data-tip]");
29
+ var target = e.target.closest("[data-tip], [data-tip-html]");
28
30
  if (!target) return;
31
+ // Only hide if we're leaving the target entirely (not moving to a child)
32
+ if (e.relatedTarget && target.contains(e.relatedTarget)) return;
29
33
  cancelShow();
30
34
  hideTooltip();
31
35
  });
@@ -80,10 +84,32 @@ function cancelShow() {
80
84
 
81
85
  function showTooltipAt(target) {
82
86
  if (!tooltipEl) return;
87
+ // Suppress tooltip when an associated popover/picker is open
88
+ var suppressSel = target.getAttribute("data-tip-suppress-when-open");
89
+ if (suppressSel) {
90
+ var openEl = document.querySelector(suppressSel);
91
+ if (openEl && !openEl.classList.contains("hidden")) return;
92
+ }
93
+ var html = target.getAttribute("data-tip-html");
83
94
  var text = target.getAttribute("data-tip");
84
- if (!text) return;
85
-
86
- tooltipEl.textContent = text;
95
+ if (!html && !text) return;
96
+ currentTarget = target;
97
+
98
+ if (html) {
99
+ tooltipEl.innerHTML = html;
100
+ tooltipEl.classList.add("multi-line");
101
+ // Render any lucide icons inside the tooltip
102
+ if (typeof window !== "undefined" && window.lucide && window.lucide.createIcons) {
103
+ window.lucide.createIcons({ icons: window.lucide.icons, nameAttr: "data-lucide", root: tooltipEl });
104
+ }
105
+ } else {
106
+ tooltipEl.textContent = text;
107
+ if (text.indexOf("\n") !== -1) {
108
+ tooltipEl.classList.add("multi-line");
109
+ } else {
110
+ tooltipEl.classList.remove("multi-line");
111
+ }
112
+ }
87
113
  tooltipEl.style.top = "-9999px";
88
114
  tooltipEl.style.left = "0";
89
115
  tooltipEl.style.right = "";
@@ -120,6 +146,7 @@ function hideTooltip() {
120
146
  if (tooltipEl) {
121
147
  tooltipEl.classList.remove("visible");
122
148
  }
149
+ currentTarget = null;
123
150
  }
124
151
 
125
152
  export { initTooltips, registerTooltip };
package/lib/sdk-bridge.js CHANGED
@@ -3,6 +3,7 @@ var fs = require("fs");
3
3
  var path = require("path");
4
4
  var { execSync } = require("child_process");
5
5
  var usersModule = require("./users");
6
+ var { getCodexConfig } = require("./codex-defaults");
6
7
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
7
8
  var { createMessageQueue } = require("./sdk-message-queue");
8
9
  var { attachMessageProcessor } = require("./sdk-message-processor");
@@ -1037,6 +1038,7 @@ function createSDKBridge(opts) {
1037
1038
  }
1038
1039
  }
1039
1040
 
1041
+ var codexConfig = getCodexConfig(sm);
1040
1042
  var queryOpts = {
1041
1043
  cwd: cwd,
1042
1044
  model: queryModel,
@@ -1058,8 +1060,8 @@ function createSDKBridge(opts) {
1058
1060
  // Codex's native approval prompts are terminal-based and cannot be
1059
1061
  // relayed through Clay's web UI, causing MCP tool calls to hang.
1060
1062
  approvalPolicy: "never",
1061
- sandboxMode: sm.codexSandbox || "workspace-write",
1062
- webSearchMode: sm.codexWebSearch || undefined,
1063
+ sandboxMode: codexConfig.sandbox,
1064
+ webSearchMode: codexConfig.webSearch,
1063
1065
  },
1064
1066
  },
1065
1067
  };
@@ -1238,7 +1240,8 @@ function createSDKBridge(opts) {
1238
1240
  }
1239
1241
  }
1240
1242
  sm.slashCommands = combined;
1241
- send({ type: "slash_commands", commands: sm.slashCommands });
1243
+ sm.setSlashCommandsForVendor(defaultVendor, combined);
1244
+ send({ type: "slash_commands", commands: combined, vendor: defaultVendor });
1242
1245
  }
1243
1246
  if (result.defaultModel) {
1244
1247
  sm.currentModel = sm.currentModel || sm._savedDefaultModel || result.defaultModel;
@@ -1266,6 +1269,7 @@ function createSDKBridge(opts) {
1266
1269
  adapters[v].init({ cwd: cwd, clayPort: clayPort, clayTls: clayTls, clayAuthToken: clayAuthToken, slug: slug }).then(function(r) {
1267
1270
  sm.modelsByVendor[v] = r.models || [];
1268
1271
  sm.capabilitiesByVendor[v] = r.capabilities || {};
1272
+ if (r.slashCommands) sm.setSlashCommandsForVendor(v, r.slashCommands);
1269
1273
  }).catch(function(e) {
1270
1274
  console.error("[sdk-bridge] warmup: " + v + " init failed:", e.message || e);
1271
1275
  });
@@ -20,6 +20,14 @@ function attachMessageProcessor(ctx) {
20
20
 
21
21
  var AUTO_TITLE_TURN_THRESHOLD = 3;
22
22
 
23
+ function getMateIdForNotification() {
24
+ if (!isMate) return null;
25
+ if (typeof slug === "string" && slug.indexOf("mate-") === 0) {
26
+ return slug.substring(5) || null;
27
+ }
28
+ return null;
29
+ }
30
+
23
31
  function sendAndRecord(session, obj) {
24
32
  sm.sendAndRecord(session, obj);
25
33
  }
@@ -404,6 +412,7 @@ function attachMessageProcessor(ctx) {
404
412
  preview: _donePreviewText,
405
413
  slug: slug,
406
414
  sessionId: session.localId,
415
+ mateId: getMateIdForNotification(),
407
416
  ownerId: session.ownerId || null,
408
417
  });
409
418
  }
package/lib/sessions.js CHANGED
@@ -3,6 +3,7 @@ var path = require("path");
3
3
  var config = require("./config");
4
4
  var utils = require("./utils");
5
5
  var users = require("./users");
6
+ var { CODEX_DEFAULTS } = require("./codex-defaults");
6
7
 
7
8
  function createSessionManager(opts) {
8
9
  var cwd = opts.cwd;
@@ -16,12 +17,16 @@ function createSessionManager(opts) {
16
17
  var nextLocalId = 1;
17
18
  var sessions = new Map(); // localId -> session object
18
19
  var activeSessionId = null; // currently active local ID
19
- var slashCommands = null; // shared across sessions
20
+ var slashCommands = null; // shared across sessions (deprecated, use slashCommandsByVendor)
21
+ var slashCommandsByVendor = {}; // vendor -> array of slash commands
20
22
  var skillNames = null; // Claude-only skills to filter from slash menu
21
23
  var singleUserUnread = {}; // sessionLocalId -> unread count (single-user mode)
22
24
  var permissionRequestIndex = {}; // requestId -> sessionLocalId (O(1) lookup)
23
25
  var capabilitiesByVendor = null; // set by sdk-bridge after adapter init
24
26
  var defaultVendor = null; // set by sdk-bridge
27
+ var codexApproval = CODEX_DEFAULTS.approval;
28
+ var codexSandbox = CODEX_DEFAULTS.sandbox;
29
+ var codexWebSearch = CODEX_DEFAULTS.webSearch;
25
30
 
26
31
  // --- Session persistence (centralized in ~/.clay/sessions/{encoded-cwd}/) ---
27
32
  var sessionsBase = path.join(config.CONFIG_DIR, "sessions");
@@ -403,8 +408,12 @@ function createSessionManager(opts) {
403
408
  var _send = (targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send;
404
409
 
405
410
  var _capsByVendor = capabilitiesByVendor || {};
406
- var _vendorCaps = _capsByVendor[session.vendor || defaultVendor || "claude"] || {};
411
+ var _sessionVendor = session.vendor || defaultVendor || "claude";
412
+ var _vendorCaps = _capsByVendor[_sessionVendor] || {};
407
413
  _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps });
414
+ // Send vendor-specific slash commands
415
+ var _vendorCmds = slashCommandsByVendor[_sessionVendor] || slashCommands || [];
416
+ _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
408
417
  broadcastSessionList();
409
418
  replayHistory(session, undefined, targetWs, transform);
410
419
 
@@ -750,12 +759,25 @@ function createSessionManager(opts) {
750
759
  get nextLocalId() { return nextLocalId; },
751
760
  get slashCommands() { return slashCommands; },
752
761
  set slashCommands(v) { slashCommands = v; },
762
+ get slashCommandsByVendor() { return slashCommandsByVendor; },
763
+ setSlashCommandsForVendor: function(vendor, cmds) {
764
+ slashCommandsByVendor[vendor] = cmds || [];
765
+ },
766
+ getSlashCommandsForVendor: function(vendor) {
767
+ return slashCommandsByVendor[vendor] || [];
768
+ },
753
769
  get skillNames() { return skillNames; },
754
770
  set skillNames(v) { skillNames = v; },
755
771
  get capabilitiesByVendor() { return capabilitiesByVendor; },
756
772
  set capabilitiesByVendor(v) { capabilitiesByVendor = v; },
757
773
  get defaultVendor() { return defaultVendor; },
758
774
  set defaultVendor(v) { defaultVendor = v; },
775
+ get codexApproval() { return codexApproval; },
776
+ set codexApproval(v) { codexApproval = v; },
777
+ get codexSandbox() { return codexSandbox; },
778
+ set codexSandbox(v) { codexSandbox = v; },
779
+ get codexWebSearch() { return codexWebSearch; },
780
+ set codexWebSearch(v) { codexWebSearch = v; },
759
781
  sessions: sessions,
760
782
  sessionsDir: sessionsDir,
761
783
  HISTORY_PAGE_SIZE: HISTORY_PAGE_SIZE,