clay-server 2.47.0-beta.4 → 2.47.0-beta.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.
@@ -10,9 +10,10 @@ import { store } from './store.js';
10
10
  import { getWs } from './ws-ref.js';
11
11
  import { closeSidebar } from './sidebar.js';
12
12
  import { showIconTooltip, hideIconTooltip, closeUserCtxMenu, getCurrentDmUserId } from './sidebar-mates.js';
13
- import { switchProject, openAddProjectModal, getCachedProjects } from './app-projects.js';
13
+ import { switchProject, openAddProjectModal } from './app-projects.js';
14
14
  import { showHomeHub } from './app-home-hub.js';
15
15
  import { getPendingTuiAttention } from './app-notifications.js';
16
+ import { aggregateFamily } from './worktree-family.js';
16
17
 
17
18
  // --- Project state ---
18
19
  var cachedProjectList = [];
@@ -31,12 +32,6 @@ var emojiPickerEl = null;
31
32
  var draggedSlug = null;
32
33
  var draggedEl = null;
33
34
 
34
- // --- Worktree folder collapse state (persisted in localStorage) ---
35
- var wtCollapsed = {};
36
- try {
37
- wtCollapsed = JSON.parse(localStorage.getItem("clay-wt-collapsed") || "{}");
38
- } catch (e) {}
39
-
40
35
  var EMOJI_CATEGORIES = [
41
36
  { id: "frequent", icon: "🕐", label: "Frequent", emojis: [
42
37
  "😀","😎","🤓","🧠","💡","🔥","⚡","🚀",
@@ -524,17 +519,8 @@ function showIconCtxMenu(anchorEl, slug, name) {
524
519
  }
525
520
  });
526
521
  menu.appendChild(removeWtItem);
527
- } else {
528
- var wtItem = document.createElement("button");
529
- wtItem.className = "project-ctx-item";
530
- wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
531
- wtItem.addEventListener("click", function (e) {
532
- e.stopPropagation();
533
- closeProjectCtxMenu();
534
- showWorktreeModal(slug, name || slug);
535
- });
536
- menu.appendChild(wtItem);
537
522
  }
523
+ // Worktree creation lives in the title-bar branch chip, not here.
538
524
 
539
525
  document.body.appendChild(menu);
540
526
  projectCtxMenu = menu;
@@ -619,20 +605,7 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
619
605
  }
620
606
  }
621
607
 
622
- var sep2 = document.createElement("div");
623
- sep2.className = "project-ctx-separator";
624
- menu.appendChild(sep2);
625
-
626
- // --- Add Worktree ---
627
- var wtItem = document.createElement("button");
628
- wtItem.className = "project-ctx-item";
629
- wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
630
- wtItem.addEventListener("click", function (e) {
631
- e.stopPropagation();
632
- closeProjectCtxMenu();
633
- showWorktreeModal(slug, name || slug);
634
- });
635
- menu.appendChild(wtItem);
608
+ // Worktree creation lives in the title-bar branch chip, not here.
636
609
 
637
610
  if (!store.get('permissions') || store.get('permissions').deleteProject !== false) {
638
611
  var sep3 = document.createElement("div");
@@ -953,13 +926,6 @@ function setupDragHandlers(el, slug) {
953
926
  });
954
927
  }
955
928
 
956
- // --- Worktree folder collapse ---
957
-
958
- function setWtCollapsed(slug, collapsed) {
959
- wtCollapsed[slug] = collapsed;
960
- try { localStorage.setItem("clay-wt-collapsed", JSON.stringify(wtCollapsed)); } catch (e) {}
961
- }
962
-
963
929
  function groupProjects(projects) {
964
930
  var parents = [];
965
931
  var wtByParent = {};
@@ -975,6 +941,21 @@ function groupProjects(projects) {
975
941
  return { parents: parents, wtByParent: wtByParent };
976
942
  }
977
943
 
944
+ function aggregateProjectFamily(parent, worktrees) {
945
+ var aggregate = aggregateFamily(parent, worktrees);
946
+ aggregate.hasFamilyPendingAttention = getPendingTuiAttention(parent.slug) > 0;
947
+ aggregate.hasWorktreeUnread = false;
948
+ aggregate.hasWorktreePending = false;
949
+ for (var i = 0; i < worktrees.length; i++) {
950
+ if (getPendingTuiAttention(worktrees[i].slug) > 0) aggregate.hasFamilyPendingAttention = true;
951
+ if (worktrees[i].unread > 0) aggregate.hasWorktreeUnread = true;
952
+ if (worktrees[i].pendingPermissions > 0 || getPendingTuiAttention(worktrees[i].slug) > 0) {
953
+ aggregate.hasWorktreePending = true;
954
+ }
955
+ }
956
+ return aggregate;
957
+ }
958
+
978
959
  // --- Icon item creation ---
979
960
 
980
961
  function createIconItem(p, currentSlug) {
@@ -1006,7 +987,7 @@ function createIconItem(p, currentSlug) {
1006
987
 
1007
988
  var projectBadge = document.createElement("span");
1008
989
  projectBadge.className = "icon-strip-project-badge";
1009
- if (p.unread > 0 && !isActive) {
990
+ if (p.unread > 0 && (!isActive || p.hasWorktreeUnread)) {
1010
991
  projectBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
1011
992
  projectBadge.classList.add("has-unread");
1012
993
  }
@@ -1016,7 +997,8 @@ function createIconItem(p, currentSlug) {
1016
997
  // broadcast — they surface via tui_attention notifications. We shake the
1017
998
  // same icon for either, so the user notices regardless of which engine
1018
999
  // is asking.
1019
- if (!isActive && (p.pendingPermissions > 0 || getPendingTuiAttention(p.slug) > 0)) {
1000
+ if ((!isActive && (p.pendingPermissions > 0 || p.hasFamilyPendingAttention || getPendingTuiAttention(p.slug) > 0)) ||
1001
+ p.hasWorktreePending) {
1020
1002
  el.classList.add("has-pending-perm");
1021
1003
  }
1022
1004
 
@@ -1037,7 +1019,7 @@ function createIconItem(p, currentSlug) {
1037
1019
 
1038
1020
  // --- Worktree creation modal ---
1039
1021
 
1040
- function showWorktreeModal(parentSlug, parentName) {
1022
+ export function showWorktreeModal(parentSlug, parentName) {
1041
1023
  var existing = document.getElementById("wt-modal-container");
1042
1024
  if (existing) existing.remove();
1043
1025
 
@@ -1192,164 +1174,17 @@ export function renderIconStrip(projects, currentSlug) {
1192
1174
  for (var i = 0; i < grouped.parents.length; i++) {
1193
1175
  var p = grouped.parents[i];
1194
1176
  var worktrees = grouped.wtByParent[p.slug] || [];
1195
- var hasWorktrees = worktrees.length > 0;
1196
-
1197
- if (!hasWorktrees) {
1198
- var el = createIconItem(p, currentSlug);
1199
- (function (slug, name, elem) {
1200
- elem.addEventListener("contextmenu", function (e) {
1201
- e.preventDefault();
1202
- e.stopPropagation();
1203
- showIconCtxMenu(elem, slug, name);
1204
- });
1205
- })(p.slug, p.name || p.slug, el);
1206
- setupDragHandlers(el, p.slug);
1207
- container.appendChild(el);
1208
- continue;
1209
- }
1210
-
1211
- // Folder group for parent + worktrees
1212
- var folder = document.createElement("div");
1213
- folder.className = "icon-strip-group";
1214
- folder.dataset.parentSlug = p.slug;
1215
- if (wtCollapsed[p.slug]) folder.classList.add("collapsed");
1216
-
1217
- if (!p.isProcessing) {
1218
- for (var wpi = 0; wpi < worktrees.length; wpi++) {
1219
- if (worktrees[wpi].isProcessing) { p.isProcessing = true; break; }
1220
- }
1221
- }
1222
-
1223
- var header = createIconItem(p, currentSlug);
1224
- header.classList.add("folder-header");
1177
+ var familyProject = aggregateProjectFamily(p, worktrees);
1178
+ var el = createIconItem(familyProject, currentSlug);
1225
1179
  (function (slug, name, elem) {
1226
1180
  elem.addEventListener("contextmenu", function (e) {
1227
1181
  e.preventDefault();
1228
1182
  e.stopPropagation();
1229
1183
  showIconCtxMenu(elem, slug, name);
1230
1184
  });
1231
- })(p.slug, p.name || p.slug, header);
1232
- setupDragHandlers(header, p.slug);
1233
-
1234
- var chevron = document.createElement("span");
1235
- chevron.className = "icon-strip-group-chevron";
1236
- chevron.innerHTML = '<i data-lucide="git-branch"></i>';
1237
- (function (parentSlug, folderEl) {
1238
- chevron.addEventListener("click", function (e) {
1239
- e.preventDefault();
1240
- e.stopPropagation();
1241
- var nowCollapsed = folderEl.classList.toggle("collapsed");
1242
- setWtCollapsed(parentSlug, nowCollapsed);
1243
- });
1244
- chevron.addEventListener("contextmenu", function (e) {
1245
- e.preventDefault();
1246
- e.stopPropagation();
1247
- });
1248
- })(p.slug, folder);
1249
- chevron.setAttribute("data-tip", "Toggle worktrees");
1250
- header.appendChild(chevron);
1251
- folder.appendChild(header);
1252
-
1253
- var itemsContainer = document.createElement("div");
1254
- itemsContainer.className = "icon-strip-group-items";
1255
-
1256
- for (var wi = 0; wi < worktrees.length; wi++) {
1257
- (function (wt) {
1258
- var wtEl = document.createElement("a");
1259
- var isWtActive = wt.slug === currentSlug && !currentDmUserId;
1260
- var isAccessible = wt.worktreeAccessible !== false;
1261
- wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (!isAccessible ? " wt-disabled" : "");
1262
- wtEl.href = "/p/" + wt.slug + "/";
1263
- wtEl.dataset.slug = wt.slug;
1264
-
1265
- if (wt.icon) {
1266
- var wtEmoji = document.createElement("span");
1267
- wtEmoji.className = "wt-branch-abbrev project-emoji";
1268
- wtEmoji.textContent = wt.icon;
1269
- parseEmojis(wtEmoji);
1270
- wtEl.appendChild(wtEmoji);
1271
- } else {
1272
- var abbrev = document.createElement("span");
1273
- abbrev.className = "wt-branch-abbrev";
1274
- abbrev.textContent = getProjectAbbrev(wt.name);
1275
- wtEl.appendChild(abbrev);
1276
- }
1277
-
1278
- var wtStatus = document.createElement("span");
1279
- wtStatus.className = "icon-strip-status";
1280
- if (wt.isProcessing) wtStatus.classList.add("processing");
1281
- wtEl.appendChild(wtStatus);
1282
-
1283
- var tooltipText = wt.name;
1284
- if (!isAccessible) {
1285
- tooltipText += " (outside project path, cannot be accessed)";
1286
- }
1287
- (function (text, elem) {
1288
- elem.addEventListener("mouseenter", function () { showIconTooltip(elem, text); });
1289
- elem.addEventListener("mouseleave", hideIconTooltip);
1290
- })(tooltipText, wtEl);
1291
-
1292
- if (isAccessible) {
1293
- (function (slug) {
1294
- wtEl.addEventListener("click", function (e) {
1295
- e.preventDefault();
1296
- if (switchProject) switchProject(slug);
1297
- });
1298
- })(wt.slug);
1299
- } else {
1300
- wtEl.addEventListener("click", function (e) {
1301
- e.preventDefault();
1302
- });
1303
- }
1304
-
1305
- if (isAccessible) {
1306
- (function (slug, name, elem) {
1307
- elem.addEventListener("contextmenu", function (e) {
1308
- e.preventDefault();
1309
- e.stopPropagation();
1310
- showIconCtxMenu(elem, slug, name);
1311
- });
1312
- })(wt.slug, wt.name, wtEl);
1313
- } else {
1314
- wtEl.addEventListener("contextmenu", function (e) {
1315
- e.preventDefault();
1316
- e.stopPropagation();
1317
- });
1318
- }
1319
-
1320
- if (!isWtActive && (wt.pendingPermissions > 0 || getPendingTuiAttention(wt.slug) > 0)) {
1321
- wtEl.classList.add("has-pending-perm");
1322
- }
1323
-
1324
- itemsContainer.appendChild(wtEl);
1325
- })(worktrees[wi]);
1326
- }
1327
-
1328
- var hasWtPendingPerm = false;
1329
- for (var wpi2 = 0; wpi2 < worktrees.length; wpi2++) {
1330
- if (worktrees[wpi2].pendingPermissions > 0 || getPendingTuiAttention(worktrees[wpi2].slug) > 0) {
1331
- hasWtPendingPerm = true;
1332
- break;
1333
- }
1334
- }
1335
- if (hasWtPendingPerm) folder.classList.remove("collapsed");
1336
-
1337
- var addBtn = document.createElement("button");
1338
- addBtn.className = "icon-strip-group-add";
1339
- addBtn.textContent = "+";
1340
- (function (parentSlug, parentName, btn) {
1341
- btn.addEventListener("click", function (e) {
1342
- e.preventDefault();
1343
- e.stopPropagation();
1344
- showWorktreeModal(parentSlug, parentName);
1345
- });
1346
- btn.addEventListener("mouseenter", function () { showIconTooltip(btn, "New worktree"); });
1347
- btn.addEventListener("mouseleave", hideIconTooltip);
1348
- })(p.slug, p.name, addBtn);
1349
- itemsContainer.appendChild(addBtn);
1350
-
1351
- folder.appendChild(itemsContainer);
1352
- container.appendChild(folder);
1185
+ })(p.slug, p.name || p.slug, el);
1186
+ setupDragHandlers(el, p.slug);
1187
+ container.appendChild(el);
1353
1188
  }
1354
1189
 
1355
1190
  // Update home icon active state
@@ -1377,50 +1212,17 @@ function renderProjectList(projects, currentSlug) {
1377
1212
  for (var i = 0; i < grouped.parents.length; i++) {
1378
1213
  var p = grouped.parents[i];
1379
1214
  var worktrees = grouped.wtByParent[p.slug] || [];
1380
-
1381
- if (worktrees.length === 0) {
1382
- list.appendChild(createMobileProjectItem(p, currentSlug, false));
1383
- continue;
1384
- }
1385
-
1386
- var folderDiv = document.createElement("div");
1387
- folderDiv.className = "mobile-project-folder";
1388
- if (wtCollapsed[p.slug]) folderDiv.classList.add("collapsed");
1389
-
1390
- var headerEl = createMobileProjectItem(p, currentSlug, false);
1391
- var chevron = document.createElement("span");
1392
- chevron.className = "mobile-folder-chevron";
1393
- chevron.innerHTML = "&#9660;";
1394
- (function (parentSlug, fDiv) {
1395
- chevron.addEventListener("click", function (e) {
1396
- e.preventDefault();
1397
- e.stopPropagation();
1398
- var nowCollapsed = fDiv.classList.toggle("collapsed");
1399
- setWtCollapsed(parentSlug, nowCollapsed);
1400
- });
1401
- })(p.slug, folderDiv);
1402
- headerEl.appendChild(chevron);
1403
- folderDiv.appendChild(headerEl);
1404
-
1405
- var wtList = document.createElement("div");
1406
- wtList.className = "mobile-folder-items";
1407
- for (var wi = 0; wi < worktrees.length; wi++) {
1408
- var isAccessible = worktrees[wi].worktreeAccessible !== false;
1409
- var wtItem = createMobileProjectItem(worktrees[wi], currentSlug, true);
1410
- if (!isAccessible) wtItem.classList.add("wt-disabled");
1411
- if (!isAccessible) {
1412
- wtItem.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); });
1413
- }
1414
- wtList.appendChild(wtItem);
1415
- }
1416
- folderDiv.appendChild(wtList);
1417
- list.appendChild(folderDiv);
1215
+ list.appendChild(createMobileProjectItem(aggregateProjectFamily(p, worktrees), currentSlug));
1418
1216
  }
1419
1217
  }
1420
1218
 
1421
- function createMobileProjectItem(p, currentSlug, isWorktree) {
1219
+ function createMobileProjectItem(p, currentSlug) {
1422
1220
  var el = document.createElement("button");
1423
- el.className = "mobile-project-item" + (p.slug === currentSlug ? " active" : "") + (isWorktree ? " wt-item" : "");
1221
+ var isActive = p.slug === currentSlug;
1222
+ el.className = "mobile-project-item" + (isActive ? " active" : "");
1223
+ if ((!isActive && (p.pendingPermissions > 0 || p.hasFamilyPendingAttention)) || p.hasWorktreePending) {
1224
+ el.classList.add("has-pending-perm");
1225
+ }
1424
1226
 
1425
1227
  var abbrev = document.createElement("span");
1426
1228
  abbrev.className = "mobile-project-abbrev";
@@ -1443,6 +1245,13 @@ function createMobileProjectItem(p, currentSlug, isWorktree) {
1443
1245
  el.appendChild(dot);
1444
1246
  }
1445
1247
 
1248
+ if (p.unread > 0 && (!isActive || p.hasWorktreeUnread)) {
1249
+ var badge = document.createElement("span");
1250
+ badge.className = "mobile-project-unread";
1251
+ badge.textContent = p.unread > 99 ? "99+" : String(p.unread);
1252
+ el.appendChild(badge);
1253
+ }
1254
+
1446
1255
  el.addEventListener("click", function () {
1447
1256
  if (switchProject) switchProject(p.slug);
1448
1257
  if (closeSidebar) closeSidebar();
@@ -1454,12 +1263,30 @@ function createMobileProjectItem(p, currentSlug, isWorktree) {
1454
1263
  export function getEmojiCategories() { return EMOJI_CATEGORIES; }
1455
1264
 
1456
1265
  export function updateProjectBadge(slug, count) {
1457
- var icon = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
1266
+ var targetSlug = slug;
1267
+ for (var i = 0; i < cachedProjectList.length; i++) {
1268
+ if (cachedProjectList[i].slug !== slug) continue;
1269
+ cachedProjectList[i].unread = count;
1270
+ if (cachedProjectList[i].isWorktree && cachedProjectList[i].parentSlug) {
1271
+ targetSlug = cachedProjectList[i].parentSlug;
1272
+ }
1273
+ break;
1274
+ }
1275
+
1276
+ var displayCount = count;
1277
+ var grouped = groupProjects(cachedProjectList);
1278
+ for (var pi = 0; pi < grouped.parents.length; pi++) {
1279
+ if (grouped.parents[pi].slug !== targetSlug) continue;
1280
+ displayCount = aggregateFamily(grouped.parents[pi], grouped.wtByParent[targetSlug] || []).unread;
1281
+ break;
1282
+ }
1283
+
1284
+ var icon = document.querySelector('.icon-strip-item[data-slug="' + targetSlug + '"]');
1458
1285
  if (!icon) return;
1459
1286
  var badge = icon.querySelector(".icon-strip-project-badge");
1460
1287
  if (!badge) return;
1461
- if (count > 0) {
1462
- badge.textContent = count > 99 ? "99+" : String(count);
1288
+ if (displayCount > 0) {
1289
+ badge.textContent = displayCount > 99 ? "99+" : String(displayCount);
1463
1290
  badge.classList.add("has-unread");
1464
1291
  } else {
1465
1292
  badge.textContent = "";
@@ -1148,6 +1148,11 @@ function renderSessionItem(s) {
1148
1148
  var textSpan = document.createElement("span");
1149
1149
  textSpan.className = "session-item-text";
1150
1150
  var textHtml = "";
1151
+ // Which agent runtime this session ran on. Legacy sessions without a
1152
+ // recorded vendor are Claude (pre-multi-vendor era).
1153
+ var itemVendor = s.vendor || "claude";
1154
+ textHtml += '<img class="session-vendor-icon" src="' + (VENDOR_AVATARS[itemVendor] || VENDOR_AVATARS.claude) +
1155
+ '" alt="" title="' + escapeHtml(VENDOR_NAMES[itemVendor] || itemVendor) + '">';
1151
1156
  if (s.isProcessing) {
1152
1157
  textHtml += '<span class="session-processing"></span>';
1153
1158
  }
@@ -0,0 +1,60 @@
1
+ // Pure helpers for presenting worktree projects as branches of one family.
2
+
3
+ function projectName(project) {
4
+ return project ? (project.title || project.project || project.name || project.slug) : "";
5
+ }
6
+
7
+ export function familyOf(projects, slug) {
8
+ var list = projects || [];
9
+ var current = null;
10
+ for (var i = 0; i < list.length; i++) {
11
+ if (list[i].slug === slug) { current = list[i]; break; }
12
+ }
13
+ if (!current) return { parent: null, worktrees: [] };
14
+
15
+ var parentSlug = current.isWorktree ? current.parentSlug : current.slug;
16
+ var parent = current.isWorktree ? null : current;
17
+ var worktrees = [];
18
+ for (var j = 0; j < list.length; j++) {
19
+ var project = list[j];
20
+ if (!parent && project.slug === parentSlug && !project.isWorktree) parent = project;
21
+ if (project.isWorktree && project.parentSlug === parentSlug) worktrees.push(project);
22
+ }
23
+ if (current.isWorktree && worktrees.indexOf(current) === -1) worktrees.push(current);
24
+ worktrees.sort(function (a, b) { return projectName(a).localeCompare(projectName(b)); });
25
+ return { parent: parent, worktrees: worktrees };
26
+ }
27
+
28
+ export function parentProjects(projects) {
29
+ return (projects || []).filter(function (project) { return !project.isWorktree; });
30
+ }
31
+
32
+ export function displayProject(projects, slug) {
33
+ var family = familyOf(projects, slug);
34
+ if (family.parent) return family.parent;
35
+ for (var i = 0; i < (projects || []).length; i++) {
36
+ if (projects[i].slug === slug) return projects[i];
37
+ }
38
+ return null;
39
+ }
40
+
41
+ export function aggregateFamily(parent, worktrees) {
42
+ var result = Object.assign({}, parent);
43
+ var children = worktrees || [];
44
+ result.isProcessing = !!result.isProcessing;
45
+ result.unread = result.unread || 0;
46
+ result.pendingPermissions = result.pendingPermissions || 0;
47
+ for (var i = 0; i < children.length; i++) {
48
+ result.isProcessing = result.isProcessing || !!children[i].isProcessing;
49
+ result.unread += children[i].unread || 0;
50
+ result.pendingPermissions += children[i].pendingPermissions || 0;
51
+ }
52
+ return result;
53
+ }
54
+
55
+ export function switcherProjectName(projects, project) {
56
+ if (!project || !project.isWorktree) return projectName(project);
57
+ var family = familyOf(projects, project.slug);
58
+ var parentName = family.parent ? projectName(family.parent) : (project.parentSlug || "Project");
59
+ return parentName + " \u2387 " + (project.branch || projectName(project));
60
+ }
package/lib/sessions.js CHANGED
@@ -1091,6 +1091,7 @@ function createSessionManager(opts) {
1091
1091
  searchSessions: searchSessions,
1092
1092
  searchSessionContent: searchSessionContent,
1093
1093
  setResolveLoopInfo: setResolveLoopInfo,
1094
+ mapSessionForClient: mapSessionForClient,
1094
1095
  migrateSessionTitles: migrateSessionTitles,
1095
1096
  setSessionVisibility: function (localId, visibility) {
1096
1097
  var session = sessions.get(localId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.47.0-beta.4",
3
+ "version": "2.47.0-beta.6",
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",