clay-server 3.4.0-beta.7 → 3.4.0-beta.9

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.
@@ -41,10 +41,10 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
41
41
  if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
42
42
  }
43
43
  if (alreadyRegistered) continue;
44
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
44
+ var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
45
45
  relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
46
46
  worktreeRegistry[parentSlug].push(wtSlug);
47
- console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.accessible ? "(accessible)" : "(inaccessible)");
47
+ console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)");
48
48
  }
49
49
  daemonSync.register(getWorktreeSyncKey(parentSlug), function () {
50
50
  rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId);
@@ -79,7 +79,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
79
79
  if (existingSlugs[ei] === wtSlug) { found = true; break; }
80
80
  }
81
81
  if (!found) {
82
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
82
+ var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
83
83
  relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
84
84
  if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
85
85
  worktreeRegistry[parentSlug].push(wtSlug);
package/lib/daemon.js CHANGED
@@ -978,7 +978,7 @@ var relay = createServer({
978
978
  if (!result.ok) return result;
979
979
  // Register the new worktree as ephemeral project
980
980
  var wtSlug = parentSlug + "--" + dirName;
981
- var wtMeta = { parentSlug: parentSlug, branch: branchName, accessible: true };
981
+ var wtMeta = { parentSlug: parentSlug, branch: branchName, external: false };
982
982
  relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
983
983
  registerWorktreeSlug(parentSlug, wtSlug);
984
984
  console.log("[daemon] Created worktree:", wtSlug, "->", result.path);
package/lib/project.js CHANGED
@@ -163,7 +163,7 @@ function createProjectContext(opts) {
163
163
  var updateChannel = opts.updateChannel || "stable";
164
164
  var osUsers = opts.osUsers || false;
165
165
  var projectOwnerId = opts.projectOwnerId || null;
166
- var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, accessible }
166
+ var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, external }
167
167
  var isMate = opts.isMate || false;
168
168
  var onCreateWorktree = opts.onCreateWorktree || null;
169
169
  var serverPort = opts.port || 2633;
@@ -1815,7 +1815,7 @@ function createProjectContext(opts) {
1815
1815
  status.isWorktree = true;
1816
1816
  status.parentSlug = worktreeMeta.parentSlug;
1817
1817
  status.branch = worktreeMeta.branch;
1818
- status.worktreeAccessible = worktreeMeta.accessible;
1818
+ status.worktreeExternal = worktreeMeta.external === true;
1819
1819
  }
1820
1820
  if (usersModule.isMultiUser()) {
1821
1821
  var seen = {};
@@ -434,6 +434,7 @@
434
434
  on hover/active, and a 4-direction drop-shadow + white glow on the
435
435
  emoji img for readability on either background. */
436
436
  .project-switcher-item .cmd-palette-item-icon {
437
+ position: relative;
437
438
  width: 44px;
438
439
  height: 44px;
439
440
  font-size: 28px;
@@ -447,6 +448,26 @@
447
448
  flex-shrink: 0;
448
449
  transition: background 0.15s, color 0.15s;
449
450
  }
451
+ .project-switcher-external-badge {
452
+ position: absolute;
453
+ right: -5px;
454
+ bottom: -5px;
455
+ width: 17px;
456
+ height: 17px;
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ border: 2px solid var(--bg);
461
+ border-radius: 50%;
462
+ background: var(--warning);
463
+ color: var(--bg);
464
+ pointer-events: none;
465
+ }
466
+ .project-switcher-item .cmd-palette-item-icon .project-switcher-external-badge .lucide {
467
+ width: 9px;
468
+ height: 9px;
469
+ stroke-width: 2.8;
470
+ }
450
471
  .project-switcher-item .cmd-palette-item-icon .lucide {
451
472
  width: 22px;
452
473
  height: 22px;
@@ -515,8 +536,7 @@
515
536
  letter-spacing: 0.5px;
516
537
  }
517
538
 
518
- /* Disabled tile (e.g. worktree with worktreeAccessible: false). Not a
519
- valid switch target, so skip highlight, skip click, visually dim. */
539
+ /* Disabled tiles are skipped by keyboard and pointer selection. */
520
540
  .project-switcher-item.disabled {
521
541
  opacity: 0.35;
522
542
  cursor: not-allowed;
@@ -1257,20 +1257,27 @@
1257
1257
  z-index: 1;
1258
1258
  }
1259
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";
1260
+ /* Worktree registered outside the main project folder. It remains usable;
1261
+ the badge communicates location without treating it as an error. */
1262
+ .worktree-external-badge {
1268
1263
  position: absolute;
1269
- font-size: 14px;
1264
+ right: 5px;
1265
+ bottom: 1px;
1266
+ width: 14px;
1267
+ height: 14px;
1268
+ display: flex;
1269
+ align-items: center;
1270
+ justify-content: center;
1271
+ border-radius: 50%;
1272
+ border: 2px solid var(--sidebar-bg);
1273
+ background: var(--warning);
1274
+ color: var(--bg);
1270
1275
  z-index: 3;
1271
- filter: grayscale(0.3);
1276
+ pointer-events: none;
1272
1277
  }
1273
1278
 
1279
+ .worktree-external-badge .lucide { width: 8px; height: 8px; stroke-width: 2.8; }
1280
+
1274
1281
  /* Group add button */
1275
1282
  .icon-strip-group-add {
1276
1283
  width: 30px;
@@ -1420,11 +1427,20 @@ select.wt-modal-input {
1420
1427
  opacity: 0.85;
1421
1428
  }
1422
1429
 
1423
- .mobile-project-item.wt-disabled {
1424
- opacity: 0.35;
1425
- cursor: not-allowed;
1430
+ .mobile-worktree-external-badge {
1431
+ width: 20px;
1432
+ height: 20px;
1433
+ display: inline-flex;
1434
+ align-items: center;
1435
+ justify-content: center;
1436
+ border-radius: 7px;
1437
+ background: color-mix(in srgb, var(--warning) 14%, transparent);
1438
+ color: var(--warning);
1439
+ flex-shrink: 0;
1426
1440
  }
1427
1441
 
1442
+ .mobile-worktree-external-badge .lucide { width: 12px; height: 12px; stroke-width: 2.4; }
1443
+
1428
1444
  .mobile-folder-chevron {
1429
1445
  font-size: 8px;
1430
1446
  color: var(--text-dimmer);
@@ -44,29 +44,64 @@
44
44
  #header-rename-btn .lucide { width: 13px; height: 13px; }
45
45
 
46
46
  #header-add-worker-btn {
47
- display: flex;
47
+ display: inline-flex;
48
48
  align-items: center;
49
49
  justify-content: center;
50
- width: 24px;
51
- height: 24px;
52
- border: 1px solid transparent;
53
- background: none;
54
- color: var(--text-dimmer);
50
+ gap: 7px;
51
+ width: auto;
52
+ height: 28px;
53
+ border: 1px solid color-mix(in srgb, var(--accent2) 28%, var(--border));
54
+ background: var(--accent2-8);
55
+ color: var(--accent2);
55
56
  cursor: pointer;
56
57
  border-radius: 8px;
57
58
  flex-shrink: 0;
58
- padding: 0;
59
- margin-left: 4px;
59
+ padding: 0 9px 0 7px;
60
+ margin-left: 0;
61
+ font-family: inherit;
62
+ font-size: 11px;
63
+ font-weight: 700;
64
+ line-height: 1;
65
+ white-space: nowrap;
60
66
  transition: color 0.15s, background 0.15s, border-color 0.15s;
61
67
  }
62
- #header-add-worker-btn:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.04); border-color: var(--border); }
63
- #header-add-worker-btn .lucide { width: 13px; height: 13px; }
68
+ #header-add-worker-btn:hover {
69
+ color: var(--accent2-hover);
70
+ background: var(--accent2-15);
71
+ border-color: color-mix(in srgb, var(--accent2) 48%, var(--border));
72
+ }
73
+ .header-worker-icon {
74
+ position: relative;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: 16px;
79
+ height: 16px;
80
+ flex-shrink: 0;
81
+ }
82
+ .header-worker-icon > .lucide { width: 15px; height: 15px; }
83
+ .header-worker-add-badge {
84
+ position: absolute;
85
+ right: -4px;
86
+ bottom: -3px;
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ width: 9px;
91
+ height: 9px;
92
+ border-radius: 50%;
93
+ background: var(--accent2);
94
+ color: var(--bg);
95
+ box-shadow: 0 0 0 2px var(--bg);
96
+ }
97
+ .header-worker-add-badge .lucide { width: 7px; height: 7px; stroke-width: 3; }
98
+ .header-worker-label { letter-spacing: 0.01em; }
64
99
  #header-add-worker-btn.hidden { display: none; }
65
100
  body.dm-mode #header-add-worker-btn { display: none; }
66
101
 
67
- /* "Close terminal" button: sits just left of the rename pencil during a live
68
- TUI session. Matches the rename button's chrome; reds out on hover since it
69
- stops the running claude (the session stays resumable). */
102
+ /* "Close terminal" button for a live TUI session. Matches the session-control
103
+ chrome; reds out on hover since it stops the running agent while preserving
104
+ the resumable session. */
70
105
  #header-tui-close-btn {
71
106
  display: flex;
72
107
  align-items: center;
@@ -980,4 +1015,3 @@ body.dm-mode #header-add-worker-btn { display: none; }
980
1015
  #config-chip .config-chip-icon { display: inline; width: 14px; height: 14px; }
981
1016
  #config-chip-label { display: none; }
982
1017
  }
983
-
@@ -23,6 +23,10 @@ body.pane-mode #sticky-notes-container {
23
23
  display: none !important;
24
24
  }
25
25
 
26
+ body.pane-mode .notif-banner-container {
27
+ display: none !important;
28
+ }
29
+
26
30
  body.pane-mode #main-area {
27
31
  border: 0;
28
32
  border-radius: 0;
@@ -373,12 +373,11 @@
373
373
  </div>
374
374
  <button id="hamburger-btn" aria-label="Toggle sidebar" title="Toggle sidebar"><i data-lucide="menu"></i></button>
375
375
  <span class="header-title" id="header-title">Connecting...</span>
376
+ <button id="header-rename-btn" type="button" title="Rename session"><i data-lucide="pencil"></i></button>
376
377
  <button id="header-info-btn" type="button" title="Session info"><i data-lucide="info"></i></button>
377
378
  <button id="header-full-access-btn" type="button" class="session-full-access hidden" role="switch" aria-checked="false" title="Skip Permissions is off — permission prompts are on">
378
379
  <span class="session-full-access-label">Skip Permissions</span><span class="session-full-access-track" aria-hidden="true"><span></span></span>
379
380
  </button>
380
- <button id="header-rename-btn" type="button" title="Rename session"><i data-lucide="pencil"></i></button>
381
- <button id="header-add-worker-btn" type="button" title="Add Worker — this session becomes the Driver of a pair"><i data-lucide="user-plus"></i></button>
382
381
  <button type="button" id="header-tui-close-btn" class="header-tui-close-btn hidden" title="Close terminal (keeps history, resume anytime)" aria-label="Close terminal"><i data-lucide="power"></i></button>
383
382
  <div id="mate-mobile-title" class="mate-mobile-title hidden">
384
383
  <img id="mate-mobile-avatar" class="mate-mobile-avatar" alt="">
@@ -389,6 +388,13 @@
389
388
  <div id="ralph-sticky" class="hidden"></div>
390
389
  <div id="debate-sticky" class="hidden"></div>
391
390
  <div class="status">
391
+ <button id="header-add-worker-btn" type="button" aria-label="Add AI Worker" title="Add an AI Worker — this session becomes the Driver of a pair">
392
+ <span class="header-worker-icon" aria-hidden="true">
393
+ <i data-lucide="bot"></i>
394
+ <span class="header-worker-add-badge"><i data-lucide="plus"></i></span>
395
+ </span>
396
+ <span class="header-worker-label">Add Worker</span>
397
+ </button>
392
398
  <button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
393
399
  <i data-lucide="type"></i>
394
400
  </button>
@@ -861,7 +861,9 @@ export function updateContextPanel() {
861
861
  hCtxEl = document.createElement("div");
862
862
  hCtxEl.className = "header-context";
863
863
  hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
864
- statusArea.insertBefore(hCtxEl, statusArea.firstChild);
864
+ var workerBtn = document.getElementById("header-add-worker-btn");
865
+ var contextAnchor = workerBtn && workerBtn.parentNode === statusArea ? workerBtn.nextSibling : statusArea.firstChild;
866
+ statusArea.insertBefore(hCtxEl, contextAnchor);
865
867
  hCtxEl.addEventListener("mouseenter", function() {
866
868
  if (store.get('richContextUsage')) {
867
869
  showCtxPopover();
@@ -322,7 +322,7 @@ export function renderProjectList() {
322
322
  isWorktree: p.isWorktree || false,
323
323
  parentSlug: p.parentSlug || null,
324
324
  branch: p.branch || null,
325
- worktreeAccessible: p.worktreeAccessible !== undefined ? p.worktreeAccessible : true,
325
+ worktreeExternal: p.worktreeExternal === true,
326
326
  };
327
327
  });
328
328
  var st = store.snap();
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { store } from './store.js';
7
7
  import { applyContextView, getContextView } from './app-panels.js';
8
+ import { forceExternalLinkToNewTab } from './pane-links.js';
8
9
 
9
10
  var panelOpen = false;
10
11
 
@@ -29,9 +30,19 @@ function togglePaneContextPanel() {
29
30
  applyContextView(panelOpen ? "panel" : "off");
30
31
  }
31
32
 
33
+ function preparePaneLink(event) {
34
+ var target = event.target;
35
+ if (!target || typeof target.closest !== "function") return;
36
+ var anchor = target.closest("a[href]");
37
+ if (!anchor || anchor.hasAttribute("download")) return;
38
+ forceExternalLinkToNewTab(anchor, window.location.href);
39
+ }
40
+
32
41
  export function initPaneBridge() {
33
42
  if (!store.get('paneMode')) return;
34
43
  panelOpen = getContextView() === "panel";
44
+ document.addEventListener("click", preparePaneLink, true);
45
+ document.addEventListener("auxclick", preparePaneLink, true);
35
46
  window.addEventListener("message", function (event) {
36
47
  if (event.origin !== window.location.origin) return;
37
48
  var msg = event.data;
@@ -0,0 +1,23 @@
1
+ export function isExternalWebLink(href, currentHref) {
2
+ if (!href) return false;
3
+ try {
4
+ var url = new URL(href, currentHref);
5
+ var currentUrl = new URL(currentHref);
6
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
7
+ return url.origin !== currentUrl.origin;
8
+ } catch (err) {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export function forceExternalLinkToNewTab(anchor, currentHref) {
14
+ if (!anchor || !isExternalWebLink(anchor.href, currentHref)) return false;
15
+
16
+ var rel = (anchor.getAttribute("rel") || "").split(/\s+/).filter(Boolean);
17
+ if (rel.indexOf("noopener") === -1) rel.push("noopener");
18
+ if (rel.indexOf("noreferrer") === -1) rel.push("noreferrer");
19
+
20
+ anchor.setAttribute("target", "_blank");
21
+ anchor.setAttribute("rel", rel.join(" "));
22
+ return true;
23
+ }
@@ -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 { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
22
23
 
23
24
  var _projectMru = []; // project slugs, most recent first
24
25
  var _mateMru = []; // mate ids (mate_XXX), most recent first
@@ -257,18 +258,16 @@ function buildProjectEntries() {
257
258
  // Keep natural (server-provided) order so the list looks the same
258
259
  // every time the switcher opens. MRU only drives initial highlight.
259
260
  return projects.map(function (p) {
260
- // Worktrees can end up "outside project path" (parent workspace
261
- // unmounted or moved); those are effectively unreachable from
262
- // this session and shouldn't be switchable.
263
- var unreachable = p.isWorktree && p.worktreeAccessible === false;
261
+ var external = isExternalWorktree(p);
264
262
  return {
265
263
  key: p.slug,
266
264
  title: p.title || p.project || p.slug,
267
265
  icon: p.icon || null,
268
266
  fallbackIcon: 'folder',
269
267
  isCurrent: p.slug === store.get('currentSlug'),
270
- disabled: unreachable,
271
- disabledReason: unreachable ? 'Outside project path' : null,
268
+ external: external,
269
+ disabled: false,
270
+ disabledReason: null,
272
271
  };
273
272
  });
274
273
  }
@@ -336,6 +335,7 @@ function buildEntryNode(entry, index) {
336
335
  item.className = 'cmd-palette-item project-switcher-item';
337
336
  if (entry.disabled) item.classList.add('disabled');
338
337
  if (entry.disabled && entry.disabledReason) item.title = entry.disabledReason;
338
+ if (entry.external) item.title = externalWorktreeTooltip(entry.title);
339
339
  item.dataset.index = String(index);
340
340
 
341
341
  var iconWrap = document.createElement('div');
@@ -353,6 +353,7 @@ function buildEntryNode(entry, index) {
353
353
  fallback.setAttribute('data-lucide', entry.fallbackIcon || 'folder');
354
354
  iconWrap.appendChild(fallback);
355
355
  }
356
+ if (entry.external) appendExternalWorktreeBadge(iconWrap, 'project-switcher-external-badge');
356
357
  item.appendChild(iconWrap);
357
358
 
358
359
  var body = document.createElement('div');
@@ -13,6 +13,7 @@ import { showIconTooltip, hideIconTooltip, closeUserCtxMenu, getCurrentDmUserId
13
13
  import { switchProject, openAddProjectModal, getCachedProjects } from './app-projects.js';
14
14
  import { showHomeHub } from './app-home-hub.js';
15
15
  import { getPendingTuiAttention } from './app-notifications.js';
16
+ import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
16
17
 
17
18
  // --- Project state ---
18
19
  var cachedProjectList = [];
@@ -489,7 +490,7 @@ export function closeProjectCtxMenu() {
489
490
  }
490
491
  }
491
492
 
492
- function showIconCtxMenu(anchorEl, slug, name) {
493
+ function showIconCtxMenu(anchorEl, slug, name, options) {
493
494
  closeProjectCtxMenu();
494
495
  if (closeUserCtxMenu) closeUserCtxMenu();
495
496
  closeEmojiPicker();
@@ -510,17 +511,19 @@ function showIconCtxMenu(anchorEl, slug, name) {
510
511
  menu.appendChild(iconItem);
511
512
 
512
513
  if (isWorktree) {
513
- var removeWtItem = document.createElement("button");
514
- removeWtItem.className = "project-ctx-item project-ctx-delete";
515
- removeWtItem.innerHTML = iconHtml("trash-2") + " <span>Remove Worktree</span>";
516
- removeWtItem.addEventListener("click", function (e) {
517
- e.stopPropagation();
518
- closeProjectCtxMenu();
519
- if (getWs() && store.get('connected')) {
520
- getWs().send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
521
- }
522
- });
523
- menu.appendChild(removeWtItem);
514
+ if (!options || options.canRemoveWorktree !== false) {
515
+ var removeWtItem = document.createElement("button");
516
+ removeWtItem.className = "project-ctx-item project-ctx-delete";
517
+ removeWtItem.innerHTML = iconHtml("trash-2") + " <span>Remove Worktree</span>";
518
+ removeWtItem.addEventListener("click", function (e) {
519
+ e.stopPropagation();
520
+ closeProjectCtxMenu();
521
+ if (getWs() && store.get('connected')) {
522
+ getWs().send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
523
+ }
524
+ });
525
+ menu.appendChild(removeWtItem);
526
+ }
524
527
  } else {
525
528
  var wtItem = document.createElement("button");
526
529
  wtItem.className = "project-ctx-item";
@@ -1253,8 +1256,8 @@ export function renderIconStrip(projects, currentSlug) {
1253
1256
  (function (wt) {
1254
1257
  var wtEl = document.createElement("a");
1255
1258
  var isWtActive = wt.slug === currentSlug && !currentDmUserId;
1256
- var isAccessible = wt.worktreeAccessible !== false;
1257
- wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (!isAccessible ? " wt-disabled" : "");
1259
+ var isExternal = isExternalWorktree(wt);
1260
+ wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (isExternal ? " wt-external" : "");
1258
1261
  wtEl.href = "/p/" + wt.slug + "/";
1259
1262
  wtEl.dataset.slug = wt.slug;
1260
1263
 
@@ -1276,42 +1279,28 @@ export function renderIconStrip(projects, currentSlug) {
1276
1279
  if (wt.isProcessing) wtStatus.classList.add("processing");
1277
1280
  wtEl.appendChild(wtStatus);
1278
1281
 
1279
- var tooltipText = wt.name;
1280
- if (!isAccessible) {
1281
- tooltipText += " (outside project path, cannot be accessed)";
1282
- }
1282
+ if (isExternal) appendExternalWorktreeBadge(wtEl, "worktree-external-badge");
1283
+
1284
+ var tooltipText = isExternal ? externalWorktreeTooltip(wt.name) : wt.name;
1283
1285
  (function (text, elem) {
1284
1286
  elem.addEventListener("mouseenter", function () { showIconTooltip(elem, text); });
1285
1287
  elem.addEventListener("mouseleave", hideIconTooltip);
1286
1288
  })(tooltipText, wtEl);
1287
1289
 
1288
- if (isAccessible) {
1289
- (function (slug) {
1290
- wtEl.addEventListener("click", function (e) {
1291
- e.preventDefault();
1292
- if (switchProject) switchProject(slug);
1293
- });
1294
- })(wt.slug);
1295
- } else {
1290
+ (function (slug) {
1296
1291
  wtEl.addEventListener("click", function (e) {
1297
1292
  e.preventDefault();
1293
+ if (switchProject) switchProject(slug);
1298
1294
  });
1299
- }
1295
+ })(wt.slug);
1300
1296
 
1301
- if (isAccessible) {
1302
- (function (slug, name, elem) {
1303
- elem.addEventListener("contextmenu", function (e) {
1304
- e.preventDefault();
1305
- e.stopPropagation();
1306
- showIconCtxMenu(elem, slug, name);
1307
- });
1308
- })(wt.slug, wt.name, wtEl);
1309
- } else {
1310
- wtEl.addEventListener("contextmenu", function (e) {
1297
+ (function (slug, name, elem, external) {
1298
+ elem.addEventListener("contextmenu", function (e) {
1311
1299
  e.preventDefault();
1312
1300
  e.stopPropagation();
1301
+ showIconCtxMenu(elem, slug, name, { canRemoveWorktree: !external });
1313
1302
  });
1314
- }
1303
+ })(wt.slug, wt.name, wtEl, isExternal);
1315
1304
 
1316
1305
  if (!isWtActive && (wt.pendingPermissions > 0 || getPendingTuiAttention(wt.slug) > 0)) {
1317
1306
  wtEl.classList.add("has-pending-perm");
@@ -1360,7 +1349,10 @@ export function renderIconStrip(projects, currentSlug) {
1360
1349
 
1361
1350
  renderProjectList(projects, currentSlug);
1362
1351
 
1363
- try { lucide.createIcons({ nodes: [container] }); } catch (e) {}
1352
+ var iconNodes = [container];
1353
+ var mobileProjectList = document.getElementById("project-list");
1354
+ if (mobileProjectList) iconNodes.push(mobileProjectList);
1355
+ try { lucide.createIcons({ nodes: iconNodes }); } catch (e) {}
1364
1356
  }
1365
1357
 
1366
1358
  function renderProjectList(projects, currentSlug) {
@@ -1401,11 +1393,10 @@ function renderProjectList(projects, currentSlug) {
1401
1393
  var wtList = document.createElement("div");
1402
1394
  wtList.className = "mobile-folder-items";
1403
1395
  for (var wi = 0; wi < worktrees.length; wi++) {
1404
- var isAccessible = worktrees[wi].worktreeAccessible !== false;
1405
1396
  var wtItem = createMobileProjectItem(worktrees[wi], currentSlug, true);
1406
- if (!isAccessible) wtItem.classList.add("wt-disabled");
1407
- if (!isAccessible) {
1408
- wtItem.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); });
1397
+ if (isExternalWorktree(worktrees[wi])) {
1398
+ wtItem.classList.add("wt-external");
1399
+ appendExternalWorktreeBadge(wtItem, "mobile-worktree-external-badge");
1409
1400
  }
1410
1401
  wtList.appendChild(wtItem);
1411
1402
  }
@@ -0,0 +1,17 @@
1
+ export function isExternalWorktree(project) {
2
+ return !!(project && project.isWorktree && project.worktreeExternal === true);
3
+ }
4
+
5
+ export function externalWorktreeTooltip(name) {
6
+ return name + " — outside the main project folder";
7
+ }
8
+
9
+ export function appendExternalWorktreeBadge(container, className) {
10
+ var badge = document.createElement("span");
11
+ badge.className = className;
12
+ badge.setAttribute("role", "img");
13
+ badge.setAttribute("aria-label", "Outside the main project folder");
14
+ badge.innerHTML = '<i data-lucide="external-link"></i>';
15
+ container.appendChild(badge);
16
+ return badge;
17
+ }
package/lib/worktree.js CHANGED
@@ -43,9 +43,14 @@ function isWorktree(projectPath) {
43
43
  }
44
44
  }
45
45
 
46
+ function isPathInside(parentPath, candidatePath) {
47
+ var relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
48
+ return relative !== "" && relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
49
+ }
50
+
46
51
  // Scan worktrees for a given project path
47
- // Returns array of { path, branch, bare, detached, accessible }
48
- // accessible = true if worktree path is inside parentPath
52
+ // Returns array of { path, branch, bare, detached, external }
53
+ // external = true when Git registered the worktree outside the main project folder
49
54
  function scanWorktrees(projectPath) {
50
55
  var resolvedParent = path.resolve(projectPath);
51
56
  try {
@@ -63,7 +68,7 @@ function scanWorktrees(projectPath) {
63
68
  if (wt.bare) continue;
64
69
  var resolvedWt = path.resolve(wt.path);
65
70
  if (resolvedWt === resolvedParent) continue;
66
- wt.accessible = resolvedWt.indexOf(resolvedParent + path.sep) === 0;
71
+ wt.external = !isPathInside(resolvedParent, resolvedWt);
67
72
  wt.dirName = path.basename(wt.path);
68
73
  results.push(wt);
69
74
  }
@@ -131,4 +136,4 @@ function removeWorktree(projectPath, worktreeDirName) {
131
136
  }
132
137
  }
133
138
 
134
- module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree };
139
+ module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree, isPathInside: isPathInside };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.7",
3
+ "version": "3.4.0-beta.9",
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",