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.
@@ -32,6 +32,7 @@ import { resetRateLimitState } from './app-rate-limit.js';
32
32
  import { closeSessionInfoPopover } from './app-header.js';
33
33
  import { resetDebateState } from './debate.js';
34
34
  import { removeDebateBottomBar } from './app-debate-ui.js';
35
+ import { familyOf as deriveFamily, displayProject } from './worktree-family.js';
35
36
 
36
37
  // --- Module-owned state ---
37
38
  var cachedProjects = [];
@@ -201,6 +202,32 @@ export function setCachedProjectCount(v) { cachedProjectCount = v; }
201
202
  export function getCachedRemovedProjects() { return cachedRemovedProjects; }
202
203
  export function setCachedRemovedProjects(v) { cachedRemovedProjects = v; }
203
204
 
205
+ export function familyOf(slug) { return deriveFamily(cachedProjects, slug); }
206
+ export function currentFamily() { return familyOf(store.get('currentSlug')); }
207
+
208
+ export function updateTitleBarProjectIdentity(fallbackName, fallbackIcon) {
209
+ if (store.get('dmMode') || store.get('mateProjectSlug')) {
210
+ var dmNameEl = document.getElementById("title-bar-project-name");
211
+ if (dmNameEl && fallbackName) dmNameEl.textContent = fallbackName;
212
+ return;
213
+ }
214
+ var identity = displayProject(cachedProjects, store.get('currentSlug'));
215
+ var name = identity ? (identity.title || identity.project || identity.slug) : fallbackName;
216
+ var icon = identity ? (identity.icon || null) : (fallbackIcon || null);
217
+ var nameEl = document.getElementById("title-bar-project-name");
218
+ if (nameEl && name) nameEl.textContent = name;
219
+ var iconEl = document.getElementById("title-bar-project-icon");
220
+ if (!iconEl) return;
221
+ if (icon) {
222
+ iconEl.textContent = icon;
223
+ parseEmojis(iconEl);
224
+ iconEl.classList.add("has-icon");
225
+ } else {
226
+ iconEl.textContent = "";
227
+ iconEl.classList.remove("has-icon");
228
+ }
229
+ }
230
+
204
231
  // --- Functions ---
205
232
 
206
233
  export function updateProjectList(msg) {
@@ -215,6 +242,7 @@ export function updateProjectList(msg) {
215
242
  _lastProjectsJson = projectsJson;
216
243
  }
217
244
  cachedProjects = msg.projects;
245
+ store.set({ projects: cachedProjects });
218
246
  }
219
247
  if (msg.removedProjects) cachedRemovedProjects = msg.removedProjects;
220
248
  else if (msg.removedProjects === undefined) { /* keep cached */ }
@@ -326,31 +354,12 @@ export function renderProjectList() {
326
354
  });
327
355
  var st = store.snap();
328
356
  var iconStripActiveSlug = (st.mateProjectSlug && st.savedMainSlug) ? st.savedMainSlug : st.currentSlug;
357
+ var activeFamily = deriveFamily(cachedProjects, iconStripActiveSlug);
358
+ if (activeFamily.parent) iconStripActiveSlug = activeFamily.parent.slug;
329
359
  renderIconStrip(iconStripProjects, iconStripActiveSlug);
330
360
  // Update title bar project name and icon if it changed
331
361
  if (!st.mateProjectSlug) {
332
- for (var pi = 0; pi < cachedProjects.length; pi++) {
333
- if (cachedProjects[pi].slug === st.currentSlug) {
334
- var updatedName = cachedProjects[pi].title || cachedProjects[pi].project;
335
- var tbName = document.getElementById("title-bar-project-name");
336
- if (tbName && updatedName) tbName.textContent = updatedName;
337
- var tbIcon = document.getElementById("title-bar-project-icon");
338
- if (tbIcon) {
339
- var pIcon = cachedProjects[pi].icon || null;
340
- if (pIcon) {
341
- tbIcon.textContent = pIcon;
342
- parseEmojis(tbIcon);
343
- tbIcon.classList.add("has-icon");
344
- try { localStorage.setItem("clay-project-icon-" + (st.currentSlug || "default"), pIcon); } catch (e) {}
345
- } else {
346
- tbIcon.textContent = "";
347
- tbIcon.classList.remove("has-icon");
348
- try { localStorage.removeItem("clay-project-icon-" + (st.currentSlug || "default")); } catch (e) {}
349
- }
350
- }
351
- break;
352
- }
353
- }
362
+ updateTitleBarProjectIdentity(store.get('projectName'), null);
354
363
  }
355
364
  // Re-apply current socket status to the active icon's dot
356
365
  var dot = getStatusDot();
@@ -557,9 +566,13 @@ function showRemoveProjectTaskDialog(slug, name, taskCount) {
557
566
  export function handleRemoveProjectResult(msg) {
558
567
  if (msg.ok) {
559
568
  var currentSlug = store.get('currentSlug');
569
+ var removedProj = null;
570
+ for (var ri = 0; ri < cachedProjects.length; ri++) {
571
+ if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
572
+ }
573
+ var isWorktree = removedProj ? !!removedProj.isWorktree : msg.slug.indexOf("--") !== -1;
560
574
  if (msg.slug === currentSlug) {
561
- var isWorktree = msg.slug.indexOf("--") !== -1;
562
- var parentSlug = isWorktree ? msg.slug.split("--")[0] : null;
575
+ var parentSlug = isWorktree ? ((removedProj && removedProj.parentSlug) || msg.slug.split("--")[0]) : null;
563
576
 
564
577
  showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
565
578
 
@@ -570,10 +583,6 @@ export function handleRemoveProjectResult(msg) {
570
583
  store.set({ connected: false });
571
584
  document.getElementById("connect-overlay").classList.add("hidden");
572
585
  if (!isWorktree) {
573
- var removedProj = null;
574
- for (var ri = 0; ri < cachedProjects.length; ri++) {
575
- if (cachedProjects[ri].slug === msg.slug) { removedProj = cachedProjects[ri]; break; }
576
- }
577
586
  if (removedProj) {
578
587
  cachedRemovedProjects.push({
579
588
  path: removedProj.path || "",
@@ -585,7 +594,7 @@ export function handleRemoveProjectResult(msg) {
585
594
  }
586
595
  cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
587
596
  cachedProjectCount = cachedProjects.length;
588
- store.set({ currentSlug: null });
597
+ store.set({ currentSlug: null, projects: cachedProjects });
589
598
  renderProjectList();
590
599
  resetClientState();
591
600
 
@@ -595,7 +604,11 @@ export function handleRemoveProjectResult(msg) {
595
604
  showHomeHub();
596
605
  }
597
606
  } else {
598
- showToast(msg.slug.indexOf("--") !== -1 ? "Worktree removed" : "Project removed", "success");
607
+ showToast(isWorktree ? "Worktree removed" : "Project removed", "success");
608
+ cachedProjects = cachedProjects.filter(function (p) { return p.slug !== msg.slug; });
609
+ cachedProjectCount = cachedProjects.length;
610
+ store.set({ projects: cachedProjects });
611
+ renderProjectList();
599
612
  }
600
613
  } else {
601
614
  showToast(msg.error || "Failed to remove project", "error");
@@ -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
 
@@ -383,6 +396,14 @@ function createMobileSessionItem(s) {
383
396
  var el = document.createElement("button");
384
397
  el.className = "mobile-session-item" + (s.active ? " active" : "");
385
398
 
399
+ // Vendor badge (legacy sessions without a vendor are Claude)
400
+ var mVendor = s.vendor || "claude";
401
+ var vendorIcon = document.createElement("img");
402
+ vendorIcon.className = "mobile-session-vendor-icon";
403
+ vendorIcon.src = VENDOR_AVATARS[mVendor] || VENDOR_AVATARS.claude;
404
+ vendorIcon.alt = "";
405
+ el.appendChild(vendorIcon);
406
+
386
407
  // Processing dot (left side, before title)
387
408
  if (s.isProcessing) {
388
409
  var dot = document.createElement("span");