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.
@@ -676,10 +676,13 @@ function attachHTTP(ctx) {
676
676
  defBr = hrRef.replace(/^origin\//, "");
677
677
  } catch (e) {}
678
678
  res.writeHead(200, { "Content-Type": "application/json" });
679
- res.end(JSON.stringify({ branches: brList, defaultBranch: defBr }));
679
+ res.end(JSON.stringify({ branches: brList, defaultBranch: defBr, isGitRepo: true }));
680
680
  } catch (e) {
681
+ // git failed: not a repository (or git missing). The branch chip
682
+ // hides itself on isGitRepo:false; the fallback branch list keeps
683
+ // the worktree modal from crashing if it is somehow reached.
681
684
  res.writeHead(200, { "Content-Type": "application/json" });
682
- res.end(JSON.stringify({ branches: ["main"], defaultBranch: "main" }));
685
+ res.end(JSON.stringify({ branches: ["main"], defaultBranch: "main", isGitRepo: false }));
683
686
  }
684
687
  return true;
685
688
  }
package/lib/public/app.js CHANGED
@@ -42,6 +42,7 @@ import { initProfile, getProfileLang } from './modules/profile.js';
42
42
  import { initUserSettings } from './modules/user-settings.js';
43
43
  import { initToolPalettes } from './modules/tool-palette.js';
44
44
  import { initProjectSwitcher } from './modules/project-switcher.js';
45
+ import { initBranchSwitcher } from './modules/branch-switcher.js';
45
46
  import { initAdmin, checkAdminAccess } from './modules/admin.js';
46
47
  import { initSessionSearch, toggleSearch, closeSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended as onSessionSearchHistoryPrepended } from './modules/session-search.js';
47
48
  import { initTooltips, registerTooltip } from './modules/tooltip.js';
@@ -85,7 +86,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
85
86
  var sendBtn = $("send-btn");
86
87
  function getStatusDot() {
87
88
  return document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status") ||
88
- document.querySelector("#icon-strip-projects .icon-strip-wt-item.active .icon-strip-status") ||
89
89
  document.querySelector("#icon-strip-users .icon-strip-mate.active .icon-strip-status");
90
90
  }
91
91
  var headerTitleEl = $("header-title");
@@ -270,6 +270,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
270
270
  projectName: projectName,
271
271
  cwd: "",
272
272
  currentSlug: currentSlug,
273
+ projects: [],
274
+ homeHubVisible: false,
273
275
  currentProjectOwnerId: null,
274
276
  isOsUsers: false,
275
277
  skipPermsEnabled: false,
@@ -461,6 +463,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
461
463
  // keydown listener registers later — capture-phase ordering doesn't
462
464
  // matter here but it keeps related bootstrap steps adjacent.
463
465
  initProjectSwitcher();
466
+ initBranchSwitcher();
464
467
 
465
468
  // --- Connect overlay (animated ASCII logo) ---
466
469
  var asciiLogoCanvas = $("ascii-logo-canvas");
@@ -223,7 +223,6 @@
223
223
 
224
224
  /* Show dot only on active item or when processing */
225
225
  .icon-strip-item.active .icon-strip-status,
226
- .icon-strip-wt-item.active .icon-strip-status,
227
226
  .icon-strip-status.processing {
228
227
  opacity: 1;
229
228
  }
@@ -248,7 +247,6 @@
248
247
 
249
248
  /* Pending permission shake */
250
249
  .icon-strip-item.has-pending-perm,
251
- .icon-strip-wt-item.has-pending-perm,
252
250
  .icon-strip-mate.has-pending-perm {
253
251
  animation: permShake 2s ease-in-out infinite;
254
252
  }
@@ -1146,155 +1144,6 @@
1146
1144
  opacity: 1;
1147
1145
  }
1148
1146
 
1149
- /* --- Worktree folder groups --- */
1150
- .icon-strip-group {
1151
- display: flex;
1152
- flex-direction: column;
1153
- align-items: center;
1154
- position: relative;
1155
- }
1156
-
1157
- .icon-strip-group .folder-header {
1158
- position: relative;
1159
- }
1160
-
1161
- .icon-strip-group-chevron {
1162
- position: absolute;
1163
- bottom: 2px;
1164
- right: 2px;
1165
- width: 16px;
1166
- height: 16px;
1167
- border-radius: 50%;
1168
- background: var(--bg-alt);
1169
- border: 1px solid var(--border);
1170
- display: flex;
1171
- align-items: center;
1172
- justify-content: center;
1173
- color: var(--text-dimmer);
1174
- pointer-events: auto;
1175
- cursor: pointer;
1176
- transition: transform 0.15s ease, background 0.15s, border-color 0.15s;
1177
- z-index: 2;
1178
- line-height: 1;
1179
- }
1180
-
1181
- .icon-strip-group-chevron i,
1182
- .icon-strip-group-chevron svg {
1183
- width: 10px;
1184
- height: 10px;
1185
- }
1186
-
1187
- .icon-strip-group-chevron:hover {
1188
- background: var(--bg-hover, var(--bg-alt));
1189
- border-color: var(--text-dimmer);
1190
- color: var(--text-secondary);
1191
- }
1192
-
1193
- .icon-strip-group.collapsed .icon-strip-group-chevron {
1194
- background: var(--border);
1195
- }
1196
-
1197
- .icon-strip-group-items {
1198
- display: flex;
1199
- flex-direction: column;
1200
- align-items: center;
1201
- overflow: hidden;
1202
- transition: max-height 0.2s ease;
1203
- max-height: 500px;
1204
- border-left: 2px solid var(--border);
1205
- margin-left: 0;
1206
- padding-left: 0;
1207
- }
1208
-
1209
- .icon-strip-group.collapsed .icon-strip-group-items {
1210
- max-height: 0;
1211
- }
1212
-
1213
- /* Worktree items (smaller than regular project icons) */
1214
- .icon-strip-wt-item {
1215
- width: 48px;
1216
- height: 40px;
1217
- display: flex;
1218
- align-items: center;
1219
- justify-content: center;
1220
- cursor: pointer;
1221
- position: relative;
1222
- text-decoration: none;
1223
- color: var(--text-secondary);
1224
- transition: background 0.15s;
1225
- }
1226
-
1227
- .icon-strip-wt-item::before {
1228
- content: "";
1229
- position: absolute;
1230
- width: 30px;
1231
- height: 30px;
1232
- border-radius: 8px;
1233
- background: var(--bg-alt);
1234
- opacity: 0.7;
1235
- transition: opacity 0.15s, background 0.15s;
1236
- }
1237
-
1238
- .icon-strip-wt-item:hover::before {
1239
- opacity: 1;
1240
- }
1241
-
1242
- .icon-strip-wt-item.active::before {
1243
- background: var(--accent);
1244
- opacity: 1;
1245
- }
1246
-
1247
- .icon-strip-wt-item.active {
1248
- color: #fff;
1249
- }
1250
-
1251
- .icon-strip-wt-item .wt-branch-abbrev {
1252
- font-size: 11px;
1253
- font-weight: 600;
1254
- letter-spacing: -0.5px;
1255
- pointer-events: none;
1256
- position: relative;
1257
- z-index: 1;
1258
- }
1259
-
1260
- /* Disabled/inaccessible worktree */
1261
- .icon-strip-wt-item.wt-disabled {
1262
- opacity: 0.35;
1263
- cursor: not-allowed;
1264
- }
1265
-
1266
- .icon-strip-wt-item.wt-disabled::after {
1267
- content: "\1F6AB";
1268
- position: absolute;
1269
- font-size: 14px;
1270
- z-index: 3;
1271
- filter: grayscale(0.3);
1272
- }
1273
-
1274
- /* Group add button */
1275
- .icon-strip-group-add {
1276
- width: 30px;
1277
- height: 30px;
1278
- border-radius: 8px;
1279
- border: 1.5px dashed var(--border);
1280
- background: transparent;
1281
- color: var(--text-dimmer);
1282
- font-size: 16px;
1283
- font-weight: 400;
1284
- cursor: pointer;
1285
- display: flex;
1286
- align-items: center;
1287
- justify-content: center;
1288
- margin: 2px 0 4px 0;
1289
- transition: border-color 0.15s, color 0.15s, background 0.15s;
1290
- }
1291
-
1292
- .icon-strip-group-add:hover {
1293
- border-color: var(--accent);
1294
- color: var(--accent);
1295
- background: rgba(var(--accent-rgb, 124, 58, 237), 0.08);
1296
- }
1297
-
1298
1147
  /* --- Worktree creation modal --- */
1299
1148
  .wt-modal-overlay {
1300
1149
  position: fixed;
@@ -1409,45 +1258,6 @@ select.wt-modal-input {
1409
1258
  cursor: not-allowed;
1410
1259
  }
1411
1260
 
1412
- /* --- Mobile worktree folder --- */
1413
- .mobile-project-folder {
1414
- display: flex;
1415
- flex-direction: column;
1416
- }
1417
-
1418
- .mobile-project-item.wt-item {
1419
- padding-left: 28px;
1420
- opacity: 0.85;
1421
- }
1422
-
1423
- .mobile-project-item.wt-disabled {
1424
- opacity: 0.35;
1425
- cursor: not-allowed;
1426
- }
1427
-
1428
- .mobile-folder-chevron {
1429
- font-size: 8px;
1430
- color: var(--text-dimmer);
1431
- margin-left: auto;
1432
- cursor: pointer;
1433
- transition: transform 0.15s ease;
1434
- padding: 4px;
1435
- }
1436
-
1437
- .mobile-project-folder.collapsed .mobile-folder-chevron {
1438
- transform: rotate(-90deg);
1439
- }
1440
-
1441
- .mobile-folder-items {
1442
- overflow: hidden;
1443
- transition: max-height 0.2s ease;
1444
- max-height: 500px;
1445
- }
1446
-
1447
- .mobile-project-folder.collapsed .mobile-folder-items {
1448
- max-height: 0;
1449
- }
1450
-
1451
1261
  /* --- Per-project presence avatars --- */
1452
1262
  /* --- Mobile: hide icon strip --- */
1453
1263
  /* --- Skeleton loading placeholders --- */
@@ -160,6 +160,10 @@
160
160
  background: rgba(var(--overlay-rgb), 0.06);
161
161
  }
162
162
 
163
+ .mobile-project-item.has-pending-perm {
164
+ animation: permShake 2s ease-in-out infinite;
165
+ }
166
+
163
167
  .mobile-project-abbrev {
164
168
  width: 32px;
165
169
  height: 32px;
@@ -272,6 +272,175 @@
272
272
  transform: rotate(180deg);
273
273
  }
274
274
 
275
+ .branch-chip {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ gap: 4px;
279
+ min-width: 0;
280
+ max-width: 132px;
281
+ /* Optical alignment: the pill reads slightly high next to the 17px/800
282
+ project name, so nudge it down within the centered flex row. */
283
+ margin-top: 2px;
284
+ padding: 4px 7px;
285
+ border: 1px solid var(--border);
286
+ border-radius: 999px;
287
+ background: var(--bg-alt);
288
+ color: var(--text-secondary);
289
+ font: 600 11px/1.2 "Pretendard", system-ui, sans-serif;
290
+ cursor: pointer;
291
+ flex-shrink: 1;
292
+ }
293
+
294
+ .branch-chip.hidden,
295
+ .branch-chip-menu.hidden {
296
+ display: none;
297
+ }
298
+
299
+ .branch-chip:hover,
300
+ .branch-chip.open {
301
+ border-color: var(--text-dimmer);
302
+ color: var(--text);
303
+ }
304
+
305
+ .branch-chip > svg {
306
+ width: 12px;
307
+ height: 12px;
308
+ flex-shrink: 0;
309
+ }
310
+
311
+ #branch-chip-label {
312
+ overflow: hidden;
313
+ text-overflow: ellipsis;
314
+ white-space: nowrap;
315
+ }
316
+
317
+ .branch-chip-chevron {
318
+ transition: transform 0.15s;
319
+ }
320
+
321
+ .branch-chip.open .branch-chip-chevron {
322
+ transform: rotate(180deg);
323
+ }
324
+
325
+ .branch-chip-menu {
326
+ position: fixed;
327
+ z-index: 10020;
328
+ width: 250px;
329
+ padding: 6px;
330
+ border: 1px solid var(--border);
331
+ border-radius: 10px;
332
+ background: var(--bg);
333
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
334
+ }
335
+
336
+ .branch-chip-row {
337
+ display: flex;
338
+ align-items: center;
339
+ border-radius: 7px;
340
+ }
341
+
342
+ .branch-chip-row:hover {
343
+ background: var(--bg-alt);
344
+ }
345
+
346
+ .branch-chip-row-main,
347
+ .branch-chip-row-action,
348
+ .branch-chip-new {
349
+ border: 0;
350
+ background: transparent;
351
+ color: var(--text-secondary);
352
+ font-family: inherit;
353
+ cursor: pointer;
354
+ }
355
+
356
+ .branch-chip-row-main {
357
+ min-width: 0;
358
+ flex: 1;
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 7px;
362
+ padding: 8px;
363
+ text-align: left;
364
+ }
365
+
366
+ .branch-chip-row-main:disabled {
367
+ opacity: 0.38;
368
+ cursor: not-allowed;
369
+ }
370
+
371
+ .branch-chip-row-main svg,
372
+ .branch-chip-row-action svg,
373
+ .branch-chip-new svg {
374
+ width: 14px;
375
+ height: 14px;
376
+ flex-shrink: 0;
377
+ }
378
+
379
+ .branch-chip-row-label {
380
+ overflow: hidden;
381
+ text-overflow: ellipsis;
382
+ white-space: nowrap;
383
+ }
384
+
385
+ .branch-chip-row.current .branch-chip-row-label {
386
+ color: var(--text);
387
+ font-weight: 700;
388
+ }
389
+
390
+ .branch-chip-row-main > .lucide-check {
391
+ margin-left: auto;
392
+ color: var(--accent);
393
+ }
394
+
395
+ .branch-chip-processing {
396
+ width: 7px;
397
+ height: 7px;
398
+ border-radius: 50%;
399
+ background: var(--success, #23a55a);
400
+ flex-shrink: 0;
401
+ }
402
+
403
+ .branch-chip-pending {
404
+ min-width: 17px;
405
+ padding: 1px 4px;
406
+ border-radius: 8px;
407
+ background: var(--danger, #e74c3c);
408
+ color: #fff;
409
+ font-size: 9px;
410
+ text-align: center;
411
+ }
412
+
413
+ .branch-chip-row-action {
414
+ display: flex;
415
+ padding: 7px;
416
+ opacity: 0;
417
+ }
418
+
419
+ .branch-chip-row:hover .branch-chip-row-action,
420
+ .branch-chip-row-action:focus-visible {
421
+ opacity: 0.7;
422
+ }
423
+
424
+ .branch-chip-row-action:hover {
425
+ color: var(--danger, #e74c3c);
426
+ opacity: 1;
427
+ }
428
+
429
+ .branch-chip-new {
430
+ width: 100%;
431
+ display: flex;
432
+ align-items: center;
433
+ gap: 7px;
434
+ margin-top: 5px;
435
+ padding: 9px 8px 7px;
436
+ border-top: 1px solid var(--border-subtle);
437
+ text-align: left;
438
+ }
439
+
440
+ .branch-chip-new:hover {
441
+ color: var(--text);
442
+ }
443
+
275
444
  .project-rename-input {
276
445
  width: 100%;
277
446
  font-family: "Pretendard", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
@@ -210,6 +210,12 @@
210
210
  <span class="title-bar-project-name" id="title-bar-project-name"></span>
211
211
  <i data-lucide="chevron-down" class="title-bar-chevron"></i>
212
212
  </button>
213
+ <button id="branch-chip" class="branch-chip hidden" type="button" aria-label="Switch branch" aria-expanded="false" aria-haspopup="menu">
214
+ <i data-lucide="git-branch"></i>
215
+ <span id="branch-chip-label"></span>
216
+ <i data-lucide="chevron-down" class="branch-chip-chevron"></i>
217
+ </button>
218
+ <div id="branch-chip-menu" class="branch-chip-menu hidden"></div>
213
219
  <span id="sidebar-presence" class="sidebar-presence"></span>
214
220
  <button id="sidebar-toggle-btn" class="sidebar-collapse-btn" title="Collapse sidebar"><i data-lucide="panel-left-close"></i></button>
215
221
  </div>
@@ -106,14 +106,6 @@ export function blinkIO() {
106
106
  var sessionDot = document.querySelector(".session-item.active .session-processing") ||
107
107
  document.querySelector(".mate-session-item.active .session-processing");
108
108
  if (sessionDot) sessionDot.classList.add("io");
109
- // If active project is a worktree, also blink the parent project dot
110
- var activeWt = document.querySelector("#icon-strip-projects .icon-strip-wt-item.active");
111
- var parentDot = null;
112
- if (activeWt) {
113
- var group = activeWt.closest(".icon-strip-group");
114
- if (group) parentDot = group.querySelector(".folder-header .icon-strip-status");
115
- if (parentDot) parentDot.classList.add("io");
116
- }
117
109
  // Mobile chat chip dot + mobile session dot
118
110
  var mobileChipDot = null;
119
111
  var _s = store.snap();
@@ -132,7 +124,6 @@ export function blinkIO() {
132
124
  var sd = document.querySelector(".session-item.active .session-processing.io") ||
133
125
  document.querySelector(".mate-session-item.active .session-processing.io");
134
126
  if (sd) sd.classList.remove("io");
135
- if (parentDot) parentDot.classList.remove("io");
136
127
  if (mobileChipDot) mobileChipDot.classList.remove("io");
137
128
  if (mobileSessionDot) mobileSessionDot.classList.remove("io");
138
129
  }, 80);
@@ -152,7 +143,7 @@ export function blinkSessionDot(sessionId) {
152
143
  export function updateCrossProjectBlink() {
153
144
  if (crossProjectBlinkTimer) { clearTimeout(crossProjectBlinkTimer); crossProjectBlinkTimer = null; }
154
145
  function doBlink() {
155
- var dots = document.querySelectorAll("#icon-strip-projects .icon-strip-item:not(.active) .icon-strip-status.processing, #icon-strip-projects .icon-strip-wt-item:not(.active) .icon-strip-status.processing, #icon-strip-users .icon-strip-mate:not(.active) .icon-strip-status.processing");
146
+ var dots = document.querySelectorAll("#icon-strip-projects .icon-strip-item:not(.active) .icon-strip-status.processing, #icon-strip-users .icon-strip-mate:not(.active) .icon-strip-status.processing");
156
147
  // Also blink mobile chat chip dots (same icon-strip-status class inside chips)
157
148
  var mobileDots = document.querySelectorAll(".mobile-chat-chip .icon-strip-status.processing");
158
149
  var allDots = [];
@@ -619,6 +619,7 @@ export function showHomeHub() {
619
619
  // anywhere via the persistent FAB (#clay-fab), not embedded here.
620
620
  if (store.get('dmMode')) exitDmMode();
621
621
  homeHubVisible = true;
622
+ store.set({ homeHubVisible: true });
622
623
  homeHub.classList.remove("hidden");
623
624
  // Show close button only if there's a project to return to
624
625
  if (hubCloseBtn) {
@@ -652,6 +653,7 @@ export function showHomeHub() {
652
653
  export function hideHomeHub() {
653
654
  if (!homeHubVisible) return;
654
655
  homeHubVisible = false;
656
+ store.set({ homeHubVisible: false });
655
657
  homeHub.classList.add("hidden");
656
658
  stopTipRotation();
657
659
  var mobileHome = document.getElementById("mobile-home-btn");
@@ -47,7 +47,7 @@ import { setStatus } from './app-connection.js';
47
47
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
48
48
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
49
49
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
50
- import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
50
+ import { updateProjectList, updateTitleBarProjectIdentity, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
51
51
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
52
52
  import { hideHomeHub, handleHubSchedules } from './app-home-hub.js';
53
53
  import { openDm, enterDmMode, exitDmMode, handleMateCreatedInApp, updateMateIconStatus, appendDmMessage, showDmTypingIndicator, buildMateInterviewPrompt } from './app-dm.js';
@@ -82,8 +82,7 @@ export function processMessage(msg) {
82
82
  // Override title bar with mate name and re-apply color
83
83
  var _mdn = (store.get('dmTargetUser').displayName || "New Mate");
84
84
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
85
- var _tbpn = document.getElementById("title-bar-project-name");
86
- if (_tbpn) _tbpn.textContent = _mdn;
85
+ updateTitleBarProjectIdentity(_mdn, null);
87
86
  var _mc2 = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
88
87
  var _tbc2 = document.querySelector(".title-bar-content");
89
88
  if (_tbc2) { _tbc2.style.background = _mc2; _tbc2.classList.add("mate-dm-active"); }
@@ -296,8 +295,7 @@ export function processMessage(msg) {
296
295
  if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
297
296
  var _mateDN = store.get('dmTargetUser').displayName || "New Mate";
298
297
  headerTitleEl.textContent = _mateDN;
299
- var tbProjectName = document.getElementById("title-bar-project-name");
300
- if (tbProjectName) tbProjectName.textContent = _mateDN;
298
+ updateTitleBarProjectIdentity(_mateDN, null);
301
299
  // Re-apply mate title bar styling (may be lost during project switch)
302
300
  var _mc = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
303
301
  var _tbc = document.querySelector(".title-bar-content");
@@ -305,8 +303,7 @@ export function processMessage(msg) {
305
303
  document.body.classList.add("mate-dm-active");
306
304
  } else {
307
305
  headerTitleEl.textContent = store.get('projectName');
308
- var tbProjectName = document.getElementById("title-bar-project-name");
309
- if (tbProjectName) tbProjectName.textContent = msg.title || store.get('projectName');
306
+ updateTitleBarProjectIdentity(msg.title || store.get('projectName'), null);
310
307
  }
311
308
  updatePageTitle();
312
309
  if (msg.version) {
@@ -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");