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.
- package/lib/daemon-projects.js +3 -3
- package/lib/daemon.js +1 -1
- package/lib/project.js +2 -2
- package/lib/public/css/command-palette.css +22 -2
- package/lib/public/css/icon-strip.css +29 -13
- package/lib/public/css/menus.css +48 -14
- package/lib/public/css/pane.css +4 -0
- package/lib/public/index.html +8 -2
- package/lib/public/modules/app-panels.js +3 -1
- package/lib/public/modules/app-projects.js +1 -1
- package/lib/public/modules/pane-bridge.js +11 -0
- package/lib/public/modules/pane-links.js +23 -0
- package/lib/public/modules/project-switcher.js +7 -6
- package/lib/public/modules/sidebar-projects.js +34 -43
- package/lib/public/modules/worktree-location.js +17 -0
- package/lib/worktree.js +9 -4
- package/package.json +1 -1
package/lib/daemon-projects.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
/*
|
|
1261
|
-
.
|
|
1262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1424
|
-
|
|
1425
|
-
|
|
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);
|
package/lib/public/css/menus.css
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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:
|
|
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 {
|
|
63
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
package/lib/public/css/pane.css
CHANGED
package/lib/public/index.html
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
getWs().
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
1257
|
-
wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1302
|
-
(function (
|
|
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
|
-
|
|
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 (
|
|
1407
|
-
|
|
1408
|
-
wtItem
|
|
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,
|
|
48
|
-
//
|
|
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.
|
|
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