clay-server 5.0.0-beta.4 → 5.0.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.
package/lib/public/app.js CHANGED
@@ -295,6 +295,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
295
295
  sessionFolderMenu: null,
296
296
  sessionFolderContext: null,
297
297
  sessionFolderModal: null,
298
+ sessionContextMenu: null,
299
+ sessionInlineRename: null,
298
300
  splitDelegations: {},
299
301
  pairResultStatus: [],
300
302
  notesTemporarilyHidden: false,
@@ -1033,6 +1033,8 @@ body:not(.home-active) #icon-strip-hint-mate { display: none; }
1033
1033
  /* --- Session inline rename --- */
1034
1034
  .session-rename-input {
1035
1035
  width: 100%;
1036
+ min-width: 0;
1037
+ box-sizing: border-box;
1036
1038
  background: var(--input-bg);
1037
1039
  border: 1px solid var(--text-dimmer);
1038
1040
  border-radius: 6px;
@@ -1043,6 +1045,17 @@ body:not(.home-active) #icon-strip-hint-mate { display: none; }
1043
1045
  outline: none;
1044
1046
  }
1045
1047
 
1048
+ .session-rename-input:focus {
1049
+ border-color: var(--link);
1050
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--link) 18%, transparent);
1051
+ }
1052
+
1053
+ .session-renaming .session-item-title,
1054
+ .session-renaming .mobile-session-title {
1055
+ overflow: visible;
1056
+ text-overflow: clip;
1057
+ }
1058
+
1046
1059
  /* --- Session loop groups --- */
1047
1060
  .session-loop-wrapper {
1048
1061
  margin-bottom: 0;
@@ -386,10 +386,16 @@
386
386
  /* The inline title editor must own the header while the keyboard is open.
387
387
  Leaving the other controls visible made its usable width nearly zero. */
388
388
  .title-bar-content.session-renaming .status,
389
- .title-bar-content.session-renaming #header-left > :not(.header-rename-input) {
389
+ .title-bar-content.session-renaming #header-left > :not(.header-title):not(.header-rename-input) {
390
390
  display: none !important;
391
391
  }
392
392
 
393
+ .title-bar-content.session-renaming .header-title {
394
+ display: flex;
395
+ flex: 1 1 100%;
396
+ overflow: visible;
397
+ }
398
+
393
399
  .title-bar-content.session-renaming .header-rename-input {
394
400
  flex: 1 1 100%;
395
401
  width: 100%;
@@ -13,6 +13,7 @@ import { getSessionUsage, setSessionUsage, getContextData, setContextData, updat
13
13
  import { onHistoryPrepended as onSessionSearchHistoryPrepended } from './session-search.js';
14
14
  import { getCachedSessions } from './sidebar-sessions.js';
15
15
  import { bindPermissionControl, renderPermissionControl } from './permission-control.js';
16
+ import { startHeaderSessionInlineRename } from './session-context-menu.js';
16
17
 
17
18
  // --- Module-owned state ---
18
19
  var sessionInfoPopover = null;
@@ -59,7 +60,20 @@ export function initHeader() {
59
60
  if (headerRenameBtn) {
60
61
  headerRenameBtn.addEventListener("click", function () {
61
62
  if (!store.get('activeSessionId')) return;
63
+ var split = store.get('splitPanes');
62
64
  var currentText = headerTitleEl.textContent;
65
+ if (!split) {
66
+ var sessions = getCachedSessions() || [];
67
+ var active = null;
68
+ for (var si = 0; si < sessions.length; si++) {
69
+ if (String(sessions[si].id) === String(store.get('activeSessionId'))) { active = sessions[si]; break; }
70
+ }
71
+ startHeaderSessionInlineRename(headerTitleEl, headerRenameBtn, active || {
72
+ id: store.get('activeSessionId'),
73
+ title: currentText,
74
+ });
75
+ return;
76
+ }
63
77
  var input = document.createElement("input");
64
78
  input.type = "text";
65
79
  input.className = "header-rename-input";
@@ -78,15 +92,11 @@ export function initHeader() {
78
92
  if (newTitle && newTitle !== currentText && ws && ws.readyState === 1) {
79
93
  // With a split group open the header shows the GROUP name, so the
80
94
  // pencil must rename the group, not the parent's last active session.
81
- var split = store.get('splitPanes');
82
95
  if (split) {
83
96
  if (split.groupId) {
84
97
  ws.send(JSON.stringify({ type: "split_group_rename", id: split.groupId, name: newTitle }));
85
98
  headerTitleEl.textContent = newTitle;
86
99
  }
87
- } else {
88
- ws.send(JSON.stringify({ type: "rename_session", id: store.get('activeSessionId'), title: newTitle }));
89
- headerTitleEl.textContent = newTitle;
90
100
  }
91
101
  }
92
102
  input.remove();
@@ -0,0 +1,209 @@
1
+ // Session context-menu lifecycle and exact-row title editing.
2
+
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+ import { showToast } from './utils.js';
6
+
7
+ function menuState() {
8
+ return store.get('sessionContextMenu');
9
+ }
10
+
11
+ function renameState() {
12
+ return store.get('sessionInlineRename');
13
+ }
14
+
15
+ export function closeSessionContextMenu(restoreFocus) {
16
+ var state = menuState();
17
+ if (!state) return;
18
+ store.set({ sessionContextMenu: null });
19
+ document.removeEventListener("pointerdown", onOutsidePress, true);
20
+ document.removeEventListener("mousedown", onOutsidePress, true);
21
+ document.removeEventListener("touchstart", onOutsidePress, true);
22
+ document.removeEventListener("keydown", onMenuKey, true);
23
+ window.removeEventListener("blur", onWindowBlur);
24
+ if (state.anchor) state.anchor.setAttribute("aria-expanded", "false");
25
+ if (state.el && state.el.isConnected) state.el.remove();
26
+ if (restoreFocus !== false && state.anchor && state.anchor.isConnected) {
27
+ state.anchor.focus({ preventScroll: true });
28
+ if (!state.anchorHadTabIndex) {
29
+ state.anchor.addEventListener("blur", function cleanupAnchorTabIndex() {
30
+ state.anchor.removeAttribute("tabindex");
31
+ }, { once: true });
32
+ }
33
+ }
34
+ if (state.anchor && restoreFocus === false && !state.anchorHadTabIndex) state.anchor.removeAttribute("tabindex");
35
+ else if (state.anchor && state.anchorHadTabIndex) state.anchor.setAttribute("tabindex", state.anchorTabIndex);
36
+ }
37
+
38
+ function onOutsidePress(event) {
39
+ var state = menuState();
40
+ if (!state) return;
41
+ if (event.target && state.el.contains(event.target)) return;
42
+ // Do not cancel the press: the outside control still receives its click.
43
+ closeSessionContextMenu(false);
44
+ }
45
+
46
+ function onWindowBlur() {
47
+ closeSessionContextMenu(false);
48
+ }
49
+
50
+ function onMenuKey(event) {
51
+ var state = menuState();
52
+ if (!state) return;
53
+ if (event.key === "Escape") {
54
+ event.preventDefault();
55
+ event.stopPropagation();
56
+ closeSessionContextMenu(true);
57
+ }
58
+ }
59
+
60
+ export function mountSessionContextMenu(menu, anchor, sessionId) {
61
+ closeSessionContextMenu(false);
62
+ menu.setAttribute("role", "menu");
63
+ var items = menu.querySelectorAll("button");
64
+ for (var i = 0; i < items.length; i++) {
65
+ items[i].type = "button";
66
+ items[i].setAttribute("role", "menuitem");
67
+ }
68
+ anchor.setAttribute("aria-haspopup", "menu");
69
+ anchor.setAttribute("aria-expanded", "true");
70
+ var anchorHadTabIndex = anchor.hasAttribute("tabindex");
71
+ var anchorTabIndex = anchor.getAttribute("tabindex");
72
+ if (!anchorHadTabIndex) anchor.setAttribute("tabindex", "-1");
73
+ store.set({ sessionContextMenu: {
74
+ el: menu,
75
+ anchor: anchor,
76
+ anchorHadTabIndex: anchorHadTabIndex,
77
+ anchorTabIndex: anchorTabIndex,
78
+ sessionId: sessionId || null,
79
+ } });
80
+ document.addEventListener("pointerdown", onOutsidePress, true);
81
+ document.addEventListener("mousedown", onOutsidePress, true);
82
+ document.addEventListener("touchstart", onOutsidePress, true);
83
+ document.addEventListener("keydown", onMenuKey, true);
84
+ window.addEventListener("blur", onWindowBlur);
85
+ var first = menu.querySelector("button:not([disabled])");
86
+ if (first) first.focus({ preventScroll: true });
87
+ }
88
+
89
+ function restoreTitle(state) {
90
+ if (!state || !state.title || !state.title.isConnected) return;
91
+ state.title.replaceChildren();
92
+ for (var i = 0; i < state.originalNodes.length; i++) state.title.appendChild(state.originalNodes[i]);
93
+ if (state.row) state.row.classList.remove("session-renaming");
94
+ if (state.control) state.control.style.display = state.controlDisplay;
95
+ }
96
+
97
+ export function cancelSessionInlineRename(restoreFocus) {
98
+ var state = renameState();
99
+ if (!state) return;
100
+ store.set({ sessionInlineRename: null });
101
+ restoreTitle(state);
102
+ if (restoreFocus && state.row && state.row.isConnected) state.row.focus({ preventScroll: true });
103
+ }
104
+
105
+ function currentRenameCanSend(state) {
106
+ var ws = getWs();
107
+ var permissions = store.get('permissions');
108
+ var sameSession = state && state.source === "header" ?
109
+ String(store.get('activeSessionId')) === String(state.sessionId) :
110
+ state && state.row && String(state.row.dataset.sessionId) === String(state.sessionId);
111
+ return !!(state && state.row && state.row.isConnected && sameSession &&
112
+ store.get('connected') && state.ws === ws && ws && ws.readyState === 1 &&
113
+ state.slug === store.get('currentSlug') &&
114
+ (!permissions || permissions.sessionRename !== false));
115
+ }
116
+
117
+ function finishRename(state, commit) {
118
+ if (!state || state !== renameState()) return;
119
+ store.set({ sessionInlineRename: null });
120
+ var nextTitle = state.input.value.trim();
121
+ restoreTitle(state);
122
+ if (!commit || !nextTitle || nextTitle === state.currentTitle) return;
123
+ if (!currentRenameCanSend(state)) {
124
+ showToast("Rename cancelled because the project, connection, or permission changed.", "warn");
125
+ return;
126
+ }
127
+ try {
128
+ state.ws.send(JSON.stringify({ type: "rename_session", id: state.sessionId, title: nextTitle }));
129
+ } catch (error) {
130
+ showToast("Session rename could not be sent.", "error");
131
+ }
132
+ }
133
+
134
+ function stopRowInteraction(event) {
135
+ event.stopPropagation();
136
+ }
137
+
138
+ function beginSessionInlineRename(row, title, control, session, source, inputClass) {
139
+ cancelSessionInlineRename(false);
140
+ if (!row || !row.isConnected || !title || !session) return false;
141
+
142
+ var originalNodes = [];
143
+ while (title.firstChild) originalNodes.push(title.removeChild(title.firstChild));
144
+ var input = document.createElement("input");
145
+ input.type = "text";
146
+ input.className = inputClass || "session-rename-input";
147
+ input.value = session.title || "New Session";
148
+ input.maxLength = 100;
149
+ input.setAttribute("aria-label", "Session title");
150
+ title.appendChild(input);
151
+ row.classList.add("session-renaming");
152
+ var controlDisplay = control ? control.style.display : "";
153
+ if (control) control.style.display = "none";
154
+
155
+ var state = {
156
+ row: row,
157
+ title: title,
158
+ input: input,
159
+ originalNodes: originalNodes,
160
+ sessionId: session.id,
161
+ currentTitle: session.title || "New Session",
162
+ control: control || null,
163
+ controlDisplay: controlDisplay,
164
+ source: source,
165
+ slug: store.get('currentSlug'),
166
+ ws: getWs(),
167
+ composing: false,
168
+ };
169
+ store.set({ sessionInlineRename: state });
170
+
171
+ input.addEventListener("compositionstart", function () { state.composing = true; });
172
+ input.addEventListener("compositionend", function () { state.composing = false; });
173
+ input.addEventListener("keydown", function (event) {
174
+ event.stopPropagation();
175
+ if (event.key === "Enter" && !state.composing && !event.isComposing && event.keyCode !== 229) {
176
+ event.preventDefault();
177
+ finishRename(state, true);
178
+ } else if (event.key === "Escape") {
179
+ event.preventDefault();
180
+ finishRename(state, false);
181
+ }
182
+ });
183
+ input.addEventListener("blur", function () { finishRename(state, true); });
184
+ ["click", "dblclick", "pointerdown", "mousedown", "touchstart", "contextmenu", "dragstart"].forEach(function (type) {
185
+ input.addEventListener(type, stopRowInteraction);
186
+ });
187
+ input.focus({ preventScroll: true });
188
+ input.select();
189
+ return true;
190
+ }
191
+
192
+ export function startSessionInlineRename(row, session) {
193
+ if (!row) return false;
194
+ var title = row.querySelector(".session-item-title") || row.querySelector(".mobile-session-title");
195
+ return beginSessionInlineRename(row, title, null, session, "row", "session-rename-input");
196
+ }
197
+
198
+ export function startHeaderSessionInlineRename(title, control, session) {
199
+ var row = title && title.closest(".title-bar-content");
200
+ return beginSessionInlineRename(row, title, control, session, "header", "header-rename-input");
201
+ }
202
+
203
+ store.subscribe(function (state, previous) {
204
+ if (state.currentSlug !== previous.currentSlug || state.dmMode !== previous.dmMode ||
205
+ (previous.connected && !state.connected)) {
206
+ closeSessionContextMenu(false);
207
+ cancelSessionInlineRename(false);
208
+ }
209
+ });
@@ -130,7 +130,48 @@ export function resetGithubWork() {
130
130
 
131
131
  function closeGithubWorkPopover() {
132
132
  var popup = document.getElementById('github-work-popover');
133
- if (popup) popup.hidePopover();
133
+ if (popup && popup._closeGithubWorkPopover) popup._closeGithubWorkPopover(false);
134
+ else if (popup) popup.remove();
135
+ }
136
+ function mountGithubWorkPopover(popup, button) {
137
+ var closed = false;
138
+ var observer = new MutationObserver(function () {
139
+ if (!button.isConnected || !popup.isConnected) dismiss(false);
140
+ });
141
+ function includes(target) {
142
+ return target === popup || target === button || popup.contains(target) || button.contains(target);
143
+ }
144
+ function outsidePress(event) {
145
+ if (!includes(event.target)) dismiss(false);
146
+ }
147
+ function keydown(event) {
148
+ if (event.key === 'Escape') dismiss(true);
149
+ }
150
+ function dismiss(restoreFocus) {
151
+ if (closed) return;
152
+ closed = true;
153
+ window.removeEventListener('pointerdown', outsidePress, true);
154
+ window.removeEventListener('touchstart', outsidePress, true);
155
+ window.removeEventListener('keydown', keydown, true);
156
+ window.removeEventListener('blur', blur);
157
+ observer.disconnect();
158
+ popup.removeEventListener('toggle', toggled);
159
+ button.setAttribute('aria-expanded', 'false');
160
+ try { popup.hidePopover(); } catch (error) {}
161
+ popup.remove();
162
+ if (restoreFocus && button.isConnected) button.focus({ preventScroll: true });
163
+ }
164
+ function blur() { dismiss(false); }
165
+ function toggled(event) {
166
+ if (event.newState === 'closed') dismiss(false);
167
+ }
168
+ popup._closeGithubWorkPopover = dismiss;
169
+ popup.addEventListener('toggle', toggled);
170
+ window.addEventListener('pointerdown', outsidePress, true);
171
+ window.addEventListener('touchstart', outsidePress, true);
172
+ window.addEventListener('keydown', keydown, true);
173
+ window.addEventListener('blur', blur);
174
+ observer.observe(document.body, { childList: true, subtree: true });
134
175
  }
135
176
  function bindGithubWorkPopover() {
136
177
  if (store.get('githubWorkPopoverBound')) return;
@@ -144,7 +185,8 @@ function bindGithubWorkPopover() {
144
185
  event.stopPropagation();
145
186
  var existing = document.getElementById('github-work-popover');
146
187
  var same = existing && button.getAttribute('aria-expanded') === 'true';
147
- if (existing) { existing.hidePopover(); existing.remove(); }
188
+ if (existing && existing._closeGithubWorkPopover) existing._closeGithubWorkPopover(false);
189
+ else if (existing) existing.remove();
148
190
  if (same) return;
149
191
  var links;
150
192
  try { links = JSON.parse(button.dataset.githubLinks); } catch (error) { return; }
@@ -157,15 +199,9 @@ function bindGithubWorkPopover() {
157
199
  popup.setAttribute('aria-label', workTitle);
158
200
  popup.innerHTML = '<strong>' + workTitle + '</strong>' + githubWorkMarkup({ githubLinks: links }, false, true);
159
201
  document.body.appendChild(popup);
202
+ mountGithubWorkPopover(popup, button);
160
203
  button.setAttribute('aria-expanded', 'true');
161
- popup.addEventListener('toggle', function (toggle) {
162
- if (toggle.newState === 'closed') {
163
- button.setAttribute('aria-expanded', 'false');
164
- if (document.activeElement === document.body && button.isConnected) button.focus();
165
- popup.remove();
166
- }
167
- });
168
- popup.showPopover();
204
+ try { popup.showPopover(); } catch (error) { popup._closeGithubWorkPopover(false); return; }
169
205
  var rect = button.getBoundingClientRect();
170
206
  var box = popup.getBoundingClientRect();
171
207
  popup.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - box.width - 8)) + 'px';
@@ -45,6 +45,7 @@ import {
45
45
  renderMobileDriverHierarchy,
46
46
  renderMobileOrphanHierarchy
47
47
  } from './sidebar-session-hierarchy.js';
48
+ import { cancelSessionInlineRename, closeSessionContextMenu } from './session-context-menu.js';
48
49
 
49
50
  // --- Mobile state ---
50
51
  var mobileChatSheetOpen = false;
@@ -768,6 +769,9 @@ export function refreshMobileChatSheet() {
768
769
  var sessionListEl = sheet.querySelector(".mobile-chat-session-list");
769
770
  if (!sessionListEl) return;
770
771
 
772
+ closeSessionContextMenu(false);
773
+ cancelSessionInlineRename(false);
774
+
771
775
  // Update chips: active state and processing dots
772
776
  var chips = sheet.querySelectorAll(".mobile-chat-chip");
773
777
  for (var i = 0; i < chips.length; i++) {
@@ -31,6 +31,12 @@ import {
31
31
  renderDesktopDriverHierarchy,
32
32
  renderDesktopOrphanHierarchy
33
33
  } from './sidebar-session-hierarchy.js';
34
+ import {
35
+ cancelSessionInlineRename,
36
+ closeSessionContextMenu,
37
+ mountSessionContextMenu,
38
+ startSessionInlineRename
39
+ } from './session-context-menu.js';
34
40
 
35
41
 
36
42
  // --- Session state ---
@@ -43,9 +49,6 @@ var countdownTimer = null;
43
49
  var countdownContainer = null;
44
50
 
45
51
  // --- Session context menu ---
46
- var sessionCtxMenu = null;
47
- var sessionCtxMenuAnchor = null;
48
- var sessionCtxSessionId = null;
49
52
  var armedDeleteSessionId = null;
50
53
  var armedDeleteTimer = null;
51
54
 
@@ -186,7 +189,6 @@ export function startNewSession(vendor, extra) {
186
189
  export function initSidebarSessions() {
187
190
 
188
191
  document.addEventListener("click", function () {
189
- closeSessionCtxMenu();
190
192
  clearArmedSessionDelete();
191
193
  });
192
194
 
@@ -228,21 +230,8 @@ export function getExpandedLoopRuns() {
228
230
 
229
231
  // --- Context menu ---
230
232
 
231
- function closeSessionCtxMenu(restoreFocus) {
232
- var anchor = sessionCtxMenuAnchor;
233
- if (anchor) anchor.setAttribute("aria-expanded", "false");
234
- sessionCtxMenuAnchor = null;
235
- if (sessionCtxMenu) {
236
- sessionCtxMenu.remove();
237
- sessionCtxMenu = null;
238
- sessionCtxSessionId = null;
239
- }
240
- if (restoreFocus && anchor && document.contains(anchor)) anchor.focus();
241
- }
242
-
243
233
  function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
244
- closeSessionCtxMenu();
245
- sessionCtxSessionId = sessionId;
234
+ closeSessionContextMenu(false);
246
235
 
247
236
  var menu = document.createElement("div");
248
237
  menu.className = "session-ctx-menu";
@@ -254,7 +243,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
254
243
  bookmarkItem.innerHTML = iconHtml(isFavorite ? "star-off" : "star") + " <span>" + (isFavorite ? "Remove from Favorites" : "Add to Favorites") + "</span>";
255
244
  bookmarkItem.addEventListener("click", function (e) {
256
245
  e.stopPropagation();
257
- closeSessionCtxMenu();
246
+ closeSessionContextMenu(false);
258
247
  toggleFavorite(sessionData);
259
248
  });
260
249
  menu.appendChild(bookmarkItem);
@@ -264,7 +253,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
264
253
  moveItem.innerHTML = iconHtml("folder-input") + " <span>Move to folder</span>";
265
254
  moveItem.addEventListener("click", function (e) {
266
255
  e.stopPropagation();
267
- closeSessionCtxMenu();
256
+ closeSessionContextMenu(false);
268
257
  openMoveToFolder(sessionData);
269
258
  });
270
259
  menu.appendChild(moveItem);
@@ -275,8 +264,8 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
275
264
  renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
276
265
  renameItem.addEventListener("click", function (e) {
277
266
  e.stopPropagation();
278
- closeSessionCtxMenu();
279
- startInlineRename(sessionId, title);
267
+ closeSessionContextMenu(false);
268
+ startSessionInlineRename(anchorBtn, sessionData);
280
269
  });
281
270
  menu.appendChild(renameItem);
282
271
 
@@ -290,7 +279,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
290
279
  visItem.innerHTML = iconHtml(isPrivate ? "eye" : "eye-off") + " <span>" + (isPrivate ? "Make Shared" : "Make Private") + "</span>";
291
280
  visItem.addEventListener("click", function (e) {
292
281
  e.stopPropagation();
293
- closeSessionCtxMenu();
282
+ closeSessionContextMenu(false);
294
283
  var newVis = isPrivate ? "shared" : "private";
295
284
  if (getWs() && store.get('connected')) {
296
285
  getWs().send(JSON.stringify({ type: "set_session_visibility", sessionId: sessionId, visibility: newVis }));
@@ -305,14 +294,14 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
305
294
  deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
306
295
  deleteItem.addEventListener("click", function (e) {
307
296
  e.stopPropagation();
308
- closeSessionCtxMenu();
297
+ closeSessionContextMenu(false);
309
298
  confirmDeleteSession({ id: sessionId, title: title });
310
299
  });
311
300
  menu.appendChild(deleteItem);
312
301
  }
313
302
 
314
303
  document.body.appendChild(menu);
315
- sessionCtxMenu = menu;
304
+ mountSessionContextMenu(menu, anchorBtn, sessionId);
316
305
  refreshIcons();
317
306
 
318
307
  // Position: fixed relative to the anchor button
@@ -335,7 +324,7 @@ export function openSessionContextMenu(anchor, session) {
335
324
  }
336
325
 
337
326
  function showSplitGroupCtxMenu(anchor, group) {
338
- closeSessionCtxMenu();
327
+ closeSessionContextMenu(false);
339
328
  var menu = document.createElement("div");
340
329
  menu.className = "session-ctx-menu";
341
330
 
@@ -344,7 +333,7 @@ function showSplitGroupCtxMenu(anchor, group) {
344
333
  renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
345
334
  renameItem.addEventListener("click", function (e) {
346
335
  e.stopPropagation();
347
- closeSessionCtxMenu();
336
+ closeSessionContextMenu(false);
348
337
  startSplitGroupInlineRename(group);
349
338
  });
350
339
  menu.appendChild(renameItem);
@@ -354,13 +343,13 @@ function showSplitGroupCtxMenu(anchor, group) {
354
343
  separateItem.innerHTML = iconHtml("columns-2") + " <span>Separate</span>";
355
344
  separateItem.addEventListener("click", function (e) {
356
345
  e.stopPropagation();
357
- closeSessionCtxMenu();
346
+ closeSessionContextMenu(false);
358
347
  separateGroup(group);
359
348
  });
360
349
  menu.appendChild(separateItem);
361
350
 
362
351
  document.body.appendChild(menu);
363
- sessionCtxMenu = menu;
352
+ mountSessionContextMenu(menu, anchor, null);
364
353
  refreshIcons();
365
354
  requestAnimationFrame(function () {
366
355
  var rect = anchor.getBoundingClientRect();
@@ -374,7 +363,7 @@ function showSplitGroupCtxMenu(anchor, group) {
374
363
  }
375
364
 
376
365
  function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
377
- closeSessionCtxMenu();
366
+ closeSessionContextMenu(false);
378
367
 
379
368
  var menu = document.createElement("div");
380
369
  menu.className = "session-ctx-menu";
@@ -384,7 +373,7 @@ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
384
373
  renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
385
374
  renameItem.addEventListener("click", function (e) {
386
375
  e.stopPropagation();
387
- closeSessionCtxMenu();
376
+ closeSessionContextMenu(false);
388
377
  startLoopInlineRename(loopId, loopName);
389
378
  });
390
379
  menu.appendChild(renameItem);
@@ -395,7 +384,7 @@ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
395
384
  deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
396
385
  deleteItem.addEventListener("click", function (e) {
397
386
  e.stopPropagation();
398
- closeSessionCtxMenu();
387
+ closeSessionContextMenu(false);
399
388
  var msg = 'Delete "' + (loopName || "Loop") + '"';
400
389
  if (childCount > 1) msg += " and its " + childCount + " sessions";
401
390
  msg += "? This cannot be undone.";
@@ -409,7 +398,7 @@ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
409
398
  }
410
399
 
411
400
  document.body.appendChild(menu);
412
- sessionCtxMenu = menu;
401
+ mountSessionContextMenu(menu, anchorBtn, null);
413
402
  refreshIcons();
414
403
 
415
404
  requestAnimationFrame(function () {
@@ -427,51 +416,12 @@ function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
427
416
 
428
417
  // --- Inline rename ---
429
418
 
430
- // A session can be on screen twice (its folder and Favorites); work on the copy
431
- // the user can actually see.
432
419
  function visibleSessionRow(sessionId) {
433
420
  var rows = getSessionListEl().querySelectorAll('.session-item[data-session-id="' + sessionId + '"]');
434
421
  for (var i = 0; i < rows.length; i++) if (rows[i].getClientRects().length) return rows[i];
435
422
  return rows[0] || null;
436
423
  }
437
424
 
438
- function startInlineRename(sessionId, currentTitle) {
439
- var el = visibleSessionRow(sessionId);
440
- if (!el) return;
441
- var textSpan = el.querySelector(".session-item-text");
442
- if (!textSpan) return;
443
-
444
- var input = document.createElement("input");
445
- input.type = "text";
446
- input.className = "session-rename-input";
447
- input.value = currentTitle || "New Session";
448
-
449
- var originalHtml = textSpan.innerHTML;
450
- textSpan.innerHTML = "";
451
- textSpan.appendChild(input);
452
- input.focus();
453
- input.select();
454
-
455
- function commitRename() {
456
- var newTitle = input.value.trim();
457
- if (newTitle && newTitle !== currentTitle && getWs() && store.get('connected')) {
458
- getWs().send(JSON.stringify({ type: "rename_session", id: sessionId, title: newTitle }));
459
- }
460
- // Restore text (server will send updated session_list)
461
- textSpan.innerHTML = originalHtml;
462
- if (newTitle && newTitle !== currentTitle) {
463
- textSpan.textContent = newTitle;
464
- }
465
- }
466
-
467
- input.addEventListener("keydown", function (e) {
468
- if (e.key === "Enter") { e.preventDefault(); commitRename(); }
469
- if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
470
- });
471
- input.addEventListener("blur", commitRename);
472
- input.addEventListener("click", function (e) { e.stopPropagation(); });
473
- }
474
-
475
425
  function startSplitGroupInlineRename(group) {
476
426
  var el = getSessionListEl().querySelector('.split-group-item[data-split-group-id="' + group.id + '"]');
477
427
  if (!el) return;
@@ -1023,6 +973,9 @@ export function renderSessionList(sessions) {
1023
973
  cachedSessions = sessions;
1024
974
  }
1025
975
 
976
+ closeSessionContextMenu(false);
977
+ cancelSessionInlineRename(false);
978
+
1026
979
  // Remember the inline folder draft before either surface is rebuilt: the mobile
1027
980
  // refresh below builds a fresh row whose queued focus must not be re-captured.
1028
981
  captureFolderInputs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"