clay-server 2.47.0-beta.5 → 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.
@@ -0,0 +1,178 @@
1
+ // Title-bar branch switcher for a project and its worktrees.
2
+
3
+ import { store } from './store.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { familyOf } from './worktree-family.js';
7
+ import { switchProject, confirmRemoveProject } from './app-projects.js';
8
+ import { showWorktreeModal } from './sidebar-projects.js';
9
+
10
+ var chip = null;
11
+ var label = null;
12
+ var menu = null;
13
+ var menuOpen = false;
14
+ var defaultBranches = {};
15
+ // slug -> true/false once /api/branches answers; undefined while unknown.
16
+ // Non-git projects hide the chip (no worktrees to manage there).
17
+ var gitRepoBySlug = {};
18
+
19
+ function projectName(project) {
20
+ return project ? (project.title || project.project || project.name || project.slug) : "";
21
+ }
22
+
23
+ function currentProject(projects) {
24
+ var slug = store.get('currentSlug');
25
+ for (var i = 0; i < projects.length; i++) {
26
+ if (projects[i].slug === slug) return projects[i];
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function defaultBranch(parent) {
32
+ return defaultBranches[parent.slug] || "default";
33
+ }
34
+
35
+ function requestDefaultBranch(parent) {
36
+ if (!parent || Object.prototype.hasOwnProperty.call(defaultBranches, parent.slug)) return;
37
+ defaultBranches[parent.slug] = null;
38
+ fetch("/p/" + encodeURIComponent(parent.slug) + "/api/branches")
39
+ .then(function (response) { return response.json(); })
40
+ .then(function (data) {
41
+ defaultBranches[parent.slug] = data.defaultBranch || "default";
42
+ gitRepoBySlug[parent.slug] = data.isGitRepo !== false;
43
+ renderBranchSwitcher();
44
+ })
45
+ .catch(function () {
46
+ defaultBranches[parent.slug] = "default";
47
+ renderBranchSwitcher();
48
+ });
49
+ }
50
+
51
+ function closeMenu() {
52
+ menuOpen = false;
53
+ if (menu) menu.classList.add("hidden");
54
+ if (chip) {
55
+ chip.classList.remove("open");
56
+ chip.setAttribute("aria-expanded", "false");
57
+ }
58
+ }
59
+
60
+ function positionMenu() {
61
+ if (!chip || !menu || !menuOpen) return;
62
+ var rect = chip.getBoundingClientRect();
63
+ var left = Math.min(rect.left, window.innerWidth - menu.offsetWidth - 8);
64
+ var top = rect.bottom + 6;
65
+ if (top + menu.offsetHeight > window.innerHeight - 8) top = rect.top - menu.offsetHeight - 6;
66
+ menu.style.left = Math.max(8, left) + "px";
67
+ menu.style.top = Math.max(8, top) + "px";
68
+ }
69
+
70
+ function branchLabel(project, parent) {
71
+ return project.isWorktree ? (project.branch || projectName(project)) : defaultBranch(parent);
72
+ }
73
+
74
+ function appendProjectRow(container, project, parent, isCurrent) {
75
+ var row = document.createElement("div");
76
+ row.className = "branch-chip-row" + (isCurrent ? " current" : "");
77
+ var inaccessible = project.isWorktree && project.worktreeAccessible === false;
78
+ var main = document.createElement("button");
79
+ main.className = "branch-chip-row-main";
80
+ main.disabled = inaccessible;
81
+ if (inaccessible) main.title = "Outside project path";
82
+ main.innerHTML = iconHtml("git-branch") + '<span class="branch-chip-row-label">' +
83
+ escapeHtml(branchLabel(project, parent)) + "</span>";
84
+ if (project.isProcessing) {
85
+ var dot = document.createElement("span");
86
+ dot.className = "branch-chip-processing";
87
+ main.appendChild(dot);
88
+ }
89
+ if (project.pendingPermissions > 0) {
90
+ var badge = document.createElement("span");
91
+ badge.className = "branch-chip-pending";
92
+ badge.textContent = project.pendingPermissions > 99 ? "99+" : String(project.pendingPermissions);
93
+ main.appendChild(badge);
94
+ }
95
+ if (isCurrent) main.insertAdjacentHTML("beforeend", iconHtml("check"));
96
+ if (!inaccessible) {
97
+ main.addEventListener("click", function () { closeMenu(); switchProject(project.slug); });
98
+ }
99
+ row.appendChild(main);
100
+
101
+ if (project.isWorktree && (!store.get('permissions') || store.get('permissions').deleteProject !== false)) {
102
+ var remove = document.createElement("button");
103
+ remove.className = "branch-chip-row-action";
104
+ remove.title = "Remove worktree";
105
+ remove.innerHTML = iconHtml("trash-2");
106
+ remove.addEventListener("click", function (event) {
107
+ event.stopPropagation();
108
+ closeMenu();
109
+ confirmRemoveProject(project.slug, projectName(project));
110
+ });
111
+ row.appendChild(remove);
112
+ }
113
+ container.appendChild(row);
114
+ }
115
+
116
+ function renderMenu(family, current) {
117
+ menu.innerHTML = "";
118
+ if (family.parent) appendProjectRow(menu, family.parent, family.parent, current.slug === family.parent.slug);
119
+ for (var i = 0; i < family.worktrees.length; i++) {
120
+ appendProjectRow(menu, family.worktrees[i], family.parent || family.worktrees[i], current.slug === family.worktrees[i].slug);
121
+ }
122
+ if (family.parent) {
123
+ var footer = document.createElement("button");
124
+ footer.className = "branch-chip-new";
125
+ footer.innerHTML = iconHtml("plus") + " <span>New worktree...</span>";
126
+ footer.addEventListener("click", function () {
127
+ closeMenu();
128
+ showWorktreeModal(family.parent.slug, projectName(family.parent));
129
+ });
130
+ menu.appendChild(footer);
131
+ }
132
+ refreshIcons();
133
+ positionMenu();
134
+ }
135
+
136
+ export function renderBranchSwitcher() {
137
+ if (!chip) return;
138
+ var projects = store.get('projects') || [];
139
+ var current = currentProject(projects);
140
+ var family = current ? familyOf(projects, current.slug) : { parent: null, worktrees: [] };
141
+ // The chip is the project's worktree management surface, so it shows even
142
+ // with zero worktrees (the menu then offers the default branch and "New
143
+ // worktree"). Hidden where there is no project context to manage, and for
144
+ // non-git projects (optimistically visible until /api/branches answers).
145
+ var gateSlug = family.parent ? family.parent.slug : (current ? current.slug : null);
146
+ var hidden = !current || store.get('dmMode') || store.get('mateProjectSlug') ||
147
+ store.get('homeHubVisible') || (gateSlug && gitRepoBySlug[gateSlug] === false);
148
+ chip.classList.toggle("hidden", hidden);
149
+ if (hidden) { closeMenu(); return; }
150
+ if (family.parent) requestDefaultBranch(family.parent);
151
+ label.textContent = branchLabel(current, family.parent || current);
152
+ if (menuOpen) renderMenu(family, current);
153
+ }
154
+
155
+ export function initBranchSwitcher() {
156
+ chip = document.getElementById("branch-chip");
157
+ label = document.getElementById("branch-chip-label");
158
+ menu = document.getElementById("branch-chip-menu");
159
+ if (!chip || !label || !menu) return;
160
+ chip.addEventListener("click", function (event) {
161
+ event.stopPropagation();
162
+ menuOpen = !menuOpen;
163
+ chip.classList.toggle("open", menuOpen);
164
+ chip.setAttribute("aria-expanded", menuOpen ? "true" : "false");
165
+ menu.classList.toggle("hidden", !menuOpen);
166
+ renderBranchSwitcher();
167
+ });
168
+ document.addEventListener("click", function (event) {
169
+ if (menuOpen && !menu.contains(event.target) && !chip.contains(event.target)) closeMenu();
170
+ });
171
+ window.addEventListener("resize", positionMenu);
172
+ store.subscribe(function (state, prev) {
173
+ if (state.projects !== prev.projects || state.currentSlug !== prev.currentSlug ||
174
+ state.dmMode !== prev.dmMode || state.mateProjectSlug !== prev.mateProjectSlug ||
175
+ state.homeHubVisible !== prev.homeHubVisible) renderBranchSwitcher();
176
+ });
177
+ renderBranchSwitcher();
178
+ }
@@ -16,6 +16,5 @@ export function getSessionListEl() { return ref("session-list"); }
16
16
 
17
17
  export function getStatusDot() {
18
18
  return document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status") ||
19
- document.querySelector("#icon-strip-projects .icon-strip-wt-item.active .icon-strip-status") ||
20
19
  document.querySelector("#icon-strip-users .icon-strip-mate.active .icon-strip-status");
21
20
  }
@@ -19,6 +19,7 @@ import { getCachedProjects, switchProject } from './app-projects.js';
19
19
  import { openDm } from './app-dm.js';
20
20
  import { mateAvatarUrl } from './avatar.js';
21
21
  import { parseEmojis } from './markdown.js';
22
+ import { switcherProjectName } from './worktree-family.js';
22
23
 
23
24
  var _projectMru = []; // project slugs, most recent first
24
25
  var _mateMru = []; // mate ids (mate_XXX), most recent first
@@ -263,7 +264,7 @@ function buildProjectEntries() {
263
264
  var unreachable = p.isWorktree && p.worktreeAccessible === false;
264
265
  return {
265
266
  key: p.slug,
266
- title: p.title || p.project || p.slug,
267
+ title: switcherProjectName(projects, p),
267
268
  icon: p.icon || null,
268
269
  fallbackIcon: 'folder',
269
270
  isCurrent: p.slug === store.get('currentSlug'),
@@ -38,6 +38,7 @@ import { showHomeHub } from './app-home-hub.js';
38
38
  import { openTerminal } from './terminal.js';
39
39
  import { requestKnowledgeList } from './mate-knowledge.js';
40
40
  import { loadRootDirectory } from './filebrowser.js';
41
+ import { aggregateFamily, familyOf, parentProjects } from './worktree-family.js';
41
42
 
42
43
  // --- Mobile state ---
43
44
  var mobileChatSheetOpen = false;
@@ -147,10 +148,22 @@ function closeMobileSheet() {
147
148
  }
148
149
 
149
150
  function renderSheetProjects(listEl) {
150
- for (var i = 0; i < getCachedProjectList().length; i++) {
151
+ var projects = getCachedProjectList();
152
+ var parents = parentProjects(projects);
153
+ var activeFamily = familyOf(projects, getCachedCurrentSlug());
154
+ var activeSlug = activeFamily.parent ? activeFamily.parent.slug : getCachedCurrentSlug();
155
+
156
+ for (var i = 0; i < parents.length; i++) {
151
157
  (function (p) {
158
+ var family = familyOf(projects, p.slug);
159
+ var displayProject = aggregateFamily(p, family.worktrees);
160
+ var hasWorktreeUnread = family.worktrees.some(function (worktree) { return worktree.unread > 0; });
161
+ var hasWorktreePending = family.worktrees.some(function (worktree) { return worktree.pendingPermissions > 0; });
152
162
  var el = document.createElement("button");
153
- el.className = "mobile-project-item" + (p.slug === getCachedCurrentSlug() ? " active" : "");
163
+ el.className = "mobile-project-item" + (p.slug === activeSlug ? " active" : "");
164
+ if ((displayProject.pendingPermissions > 0 && p.slug !== activeSlug) || hasWorktreePending) {
165
+ el.classList.add("has-pending-perm");
166
+ }
154
167
 
155
168
  var abbrev = document.createElement("span");
156
169
  abbrev.className = "mobile-project-abbrev";
@@ -167,16 +180,16 @@ function renderSheetProjects(listEl) {
167
180
  name.textContent = p.name;
168
181
  el.appendChild(name);
169
182
 
170
- if (p.isProcessing) {
183
+ if (displayProject.isProcessing) {
171
184
  var dot = document.createElement("span");
172
185
  dot.className = "mobile-project-processing";
173
186
  el.appendChild(dot);
174
187
  }
175
188
 
176
- if (p.unread > 0 && p.slug !== getCachedCurrentSlug()) {
189
+ if (displayProject.unread > 0 && (p.slug !== activeSlug || hasWorktreeUnread)) {
177
190
  var mBadge = document.createElement("span");
178
191
  mBadge.className = "mobile-project-unread";
179
- mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
192
+ mBadge.textContent = displayProject.unread > 99 ? "99+" : String(displayProject.unread);
180
193
  el.appendChild(mBadge);
181
194
  }
182
195
 
@@ -186,7 +199,7 @@ function renderSheetProjects(listEl) {
186
199
  });
187
200
 
188
201
  listEl.appendChild(el);
189
- })(getCachedProjectList()[i]);
202
+ })(parents[i]);
190
203
  }
191
204
  }
192
205
 
@@ -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 = "";