clay-server 2.11.0 → 2.12.0-beta.1
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/bin/cli.js +16 -4
- package/lib/daemon.js +167 -0
- package/lib/project.js +83 -1
- package/lib/public/app.js +567 -20
- package/lib/public/css/icon-strip.css +308 -5
- package/lib/public/css/menus.css +1 -16
- package/lib/public/css/messages.css +7 -0
- package/lib/public/css/session-search.css +150 -0
- package/lib/public/css/sidebar.css +30 -0
- package/lib/public/css/tooltip.css +20 -0
- package/lib/public/index.html +2 -1
- package/lib/public/modules/notifications.js +1 -58
- package/lib/public/modules/session-search.js +440 -0
- package/lib/public/modules/sidebar.js +576 -148
- package/lib/public/modules/tooltip.js +123 -0
- package/lib/public/style.css +2 -0
- package/lib/server.js +46 -3
- package/lib/sessions.js +37 -0
- package/lib/worktree.js +134 -0
- package/package.json +1 -1
|
@@ -1240,38 +1240,34 @@ export function populateCliSessionList(sessions) {
|
|
|
1240
1240
|
// --- Search hit timeline (right-side markers) ---
|
|
1241
1241
|
var searchTimelineScrollHandler = null;
|
|
1242
1242
|
var activeSearchQuery = ""; // query active in the timeline
|
|
1243
|
+
var pendingSearchScrollTarget = null; // { historyIndex, snippet, query } for scroll after history load
|
|
1243
1244
|
|
|
1244
1245
|
export function getActiveSearchQuery() {
|
|
1245
1246
|
return searchQuery;
|
|
1246
1247
|
}
|
|
1247
1248
|
|
|
1249
|
+
// Request server-side content search for the active session
|
|
1248
1250
|
export function buildSearchTimeline(query) {
|
|
1249
1251
|
removeSearchTimeline();
|
|
1250
1252
|
if (!query) return;
|
|
1251
1253
|
activeSearchQuery = query;
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
// Collect all message elements that contain the query
|
|
1257
|
-
var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
|
|
1258
|
-
var hits = [];
|
|
1259
|
-
for (var i = 0; i < allMsgs.length; i++) {
|
|
1260
|
-
var msgEl = allMsgs[i];
|
|
1261
|
-
var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content");
|
|
1262
|
-
if (!textEl) continue;
|
|
1263
|
-
var text = textEl.textContent || "";
|
|
1264
|
-
if (text.toLowerCase().indexOf(q) === -1) continue;
|
|
1265
|
-
|
|
1266
|
-
// Extract a snippet around the match
|
|
1267
|
-
var idx = text.toLowerCase().indexOf(q);
|
|
1268
|
-
var start = Math.max(0, idx - 10);
|
|
1269
|
-
var end = Math.min(text.length, idx + query.length + 10);
|
|
1270
|
-
var snippet = (start > 0 ? "\u2026" : "") + text.substring(start, end) + (end < text.length ? "\u2026" : "");
|
|
1271
|
-
hits.push({ el: msgEl, snippet: snippet });
|
|
1254
|
+
// Request full-history search from server
|
|
1255
|
+
if (ctx.ws && ctx.connected) {
|
|
1256
|
+
ctx.ws.send(JSON.stringify({ type: "search_session_content", query: query }));
|
|
1272
1257
|
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Handle server response with full-history search results
|
|
1261
|
+
export function handleSearchContentResults(msg) {
|
|
1262
|
+
if (msg.query !== activeSearchQuery) return; // stale response
|
|
1263
|
+
var savedQuery = activeSearchQuery;
|
|
1264
|
+
removeSearchTimeline();
|
|
1265
|
+
if (!msg.hits || msg.hits.length === 0) return;
|
|
1266
|
+
activeSearchQuery = savedQuery;
|
|
1273
1267
|
|
|
1274
|
-
|
|
1268
|
+
var hits = msg.hits;
|
|
1269
|
+
var total = msg.total;
|
|
1270
|
+
var messagesEl = ctx.messagesEl;
|
|
1275
1271
|
|
|
1276
1272
|
var timeline = document.createElement("div");
|
|
1277
1273
|
timeline.className = "search-timeline";
|
|
@@ -1279,6 +1275,7 @@ export function buildSearchTimeline(query) {
|
|
|
1279
1275
|
|
|
1280
1276
|
var track = document.createElement("div");
|
|
1281
1277
|
track.className = "rewind-timeline-track";
|
|
1278
|
+
track.dataset.historyTotal = total;
|
|
1282
1279
|
|
|
1283
1280
|
var viewport = document.createElement("div");
|
|
1284
1281
|
viewport.className = "rewind-timeline-viewport";
|
|
@@ -1286,7 +1283,8 @@ export function buildSearchTimeline(query) {
|
|
|
1286
1283
|
|
|
1287
1284
|
for (var i = 0; i < hits.length; i++) {
|
|
1288
1285
|
var hit = hits[i];
|
|
1289
|
-
|
|
1286
|
+
// Position based on historyIndex relative to total history length
|
|
1287
|
+
var pct = total <= 1 ? 50 : 6 + (hit.historyIndex / (total - 1)) * 88;
|
|
1290
1288
|
|
|
1291
1289
|
var snippetText = hit.snippet;
|
|
1292
1290
|
if (snippetText.length > 24) snippetText = snippetText.substring(0, 24) + "\u2026";
|
|
@@ -1295,16 +1293,14 @@ export function buildSearchTimeline(query) {
|
|
|
1295
1293
|
marker.className = "rewind-timeline-marker search-hit-marker";
|
|
1296
1294
|
marker.innerHTML = iconHtml("search") + '<span class="marker-text">' + escapeHtml(snippetText) + '</span>';
|
|
1297
1295
|
marker.style.top = pct + "%";
|
|
1298
|
-
|
|
1296
|
+
// Store historyIndex for click handling and viewport check
|
|
1297
|
+
marker.dataset.historyIndex = hit.historyIndex;
|
|
1299
1298
|
|
|
1300
|
-
(function(
|
|
1299
|
+
(function(hitData, markerEl) {
|
|
1301
1300
|
markerEl.addEventListener("click", function() {
|
|
1302
|
-
|
|
1303
|
-
targetEl.classList.remove("search-blink");
|
|
1304
|
-
void targetEl.offsetWidth; // force reflow
|
|
1305
|
-
targetEl.classList.add("search-blink");
|
|
1301
|
+
scrollToSearchHit(hitData.historyIndex, hitData.snippet, msg.query);
|
|
1306
1302
|
});
|
|
1307
|
-
})(hit
|
|
1303
|
+
})(hit, marker);
|
|
1308
1304
|
|
|
1309
1305
|
track.appendChild(marker);
|
|
1310
1306
|
}
|
|
@@ -1330,32 +1326,86 @@ export function buildSearchTimeline(query) {
|
|
|
1330
1326
|
updateSearchTimelineViewport(track, viewport);
|
|
1331
1327
|
}
|
|
1332
1328
|
|
|
1329
|
+
function scrollToSearchHit(historyIndex, snippet, query) {
|
|
1330
|
+
var historyFrom = ctx.getHistoryFrom ? ctx.getHistoryFrom() : 0;
|
|
1331
|
+
if (historyIndex < historyFrom) {
|
|
1332
|
+
// Need to load older history first
|
|
1333
|
+
pendingSearchScrollTarget = { historyIndex: historyIndex, snippet: snippet, query: query };
|
|
1334
|
+
if (ctx.ws && ctx.connected) {
|
|
1335
|
+
ctx.ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom, target: historyIndex }));
|
|
1336
|
+
}
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
// History is loaded, find matching element in DOM
|
|
1340
|
+
findAndScrollToMatch(snippet, query);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function findAndScrollToMatch(snippet, query) {
|
|
1344
|
+
var messagesEl = ctx.messagesEl;
|
|
1345
|
+
var q = query.toLowerCase();
|
|
1346
|
+
var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
|
|
1347
|
+
for (var i = 0; i < allMsgs.length; i++) {
|
|
1348
|
+
var msgEl = allMsgs[i];
|
|
1349
|
+
var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content");
|
|
1350
|
+
if (!textEl) continue;
|
|
1351
|
+
var text = textEl.textContent || "";
|
|
1352
|
+
if (text.toLowerCase().indexOf(q) === -1) continue;
|
|
1353
|
+
// Check if the snippet content matches (strip ellipsis for comparison)
|
|
1354
|
+
var cleanSnippet = snippet.replace(/^\u2026/, "").replace(/\u2026$/, "");
|
|
1355
|
+
if (text.indexOf(cleanSnippet) !== -1) {
|
|
1356
|
+
msgEl.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1357
|
+
msgEl.classList.remove("search-blink");
|
|
1358
|
+
void msgEl.offsetWidth;
|
|
1359
|
+
msgEl.classList.add("search-blink");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
// Fallback: scroll to any element containing the query text
|
|
1364
|
+
for (var j = 0; j < allMsgs.length; j++) {
|
|
1365
|
+
var el = allMsgs[j];
|
|
1366
|
+
var tEl = el.querySelector(".bubble") || el.querySelector(".md-content");
|
|
1367
|
+
if (!tEl) continue;
|
|
1368
|
+
if ((tEl.textContent || "").toLowerCase().indexOf(q) !== -1) {
|
|
1369
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1370
|
+
el.classList.remove("search-blink");
|
|
1371
|
+
void el.offsetWidth;
|
|
1372
|
+
el.classList.add("search-blink");
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Called after history_prepend completes, to scroll to pending target
|
|
1379
|
+
export function onHistoryPrepended() {
|
|
1380
|
+
if (!pendingSearchScrollTarget) return;
|
|
1381
|
+
var target = pendingSearchScrollTarget;
|
|
1382
|
+
pendingSearchScrollTarget = null;
|
|
1383
|
+
requestAnimationFrame(function() {
|
|
1384
|
+
findAndScrollToMatch(target.snippet, target.query);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1333
1388
|
function updateSearchTimelineViewport(track, viewport) {
|
|
1334
1389
|
if (!track) return;
|
|
1335
1390
|
var messagesEl = ctx.messagesEl;
|
|
1336
1391
|
var scrollH = messagesEl.scrollHeight;
|
|
1337
1392
|
var viewH = messagesEl.clientHeight;
|
|
1338
|
-
if (scrollH <= viewH) {
|
|
1339
|
-
viewport.style.top = "0";
|
|
1340
|
-
viewport.style.height = "100%";
|
|
1341
|
-
} else {
|
|
1342
|
-
var viewTop = messagesEl.scrollTop / scrollH;
|
|
1343
|
-
var viewBot = (messagesEl.scrollTop + viewH) / scrollH;
|
|
1344
|
-
viewport.style.top = (viewTop * 100) + "%";
|
|
1345
|
-
viewport.style.height = ((viewBot - viewTop) * 100) + "%";
|
|
1346
|
-
}
|
|
1347
1393
|
|
|
1348
|
-
|
|
1349
|
-
var
|
|
1350
|
-
var
|
|
1394
|
+
// Map the visible scroll area to the timeline range (6% to 94%)
|
|
1395
|
+
var historyFrom = ctx.getHistoryFrom ? ctx.getHistoryFrom() : 0;
|
|
1396
|
+
var total = parseInt(track.dataset.historyTotal || "0", 10) || 1;
|
|
1397
|
+
var timelineStart = 6 + (historyFrom / (total - 1 || 1)) * 88;
|
|
1398
|
+
var timelineEnd = 94;
|
|
1399
|
+
var timelineRange = timelineEnd - timelineStart;
|
|
1351
1400
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1401
|
+
if (scrollH <= viewH) {
|
|
1402
|
+
viewport.style.top = timelineStart + "%";
|
|
1403
|
+
viewport.style.height = timelineRange + "%";
|
|
1404
|
+
} else {
|
|
1405
|
+
var scrollFrac = messagesEl.scrollTop / scrollH;
|
|
1406
|
+
var viewFrac = viewH / scrollH;
|
|
1407
|
+
viewport.style.top = (timelineStart + scrollFrac * timelineRange) + "%";
|
|
1408
|
+
viewport.style.height = (viewFrac * timelineRange) + "%";
|
|
1359
1409
|
}
|
|
1360
1410
|
}
|
|
1361
1411
|
|
|
@@ -1694,33 +1744,62 @@ function showIconCtxMenu(anchorEl, slug, name) {
|
|
|
1694
1744
|
var menu = document.createElement("div");
|
|
1695
1745
|
menu.className = "project-ctx-menu";
|
|
1696
1746
|
|
|
1697
|
-
var
|
|
1698
|
-
iconItem.className = "project-ctx-item";
|
|
1699
|
-
iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
|
|
1700
|
-
iconItem.addEventListener("click", function (e) {
|
|
1701
|
-
e.stopPropagation();
|
|
1702
|
-
closeProjectCtxMenu();
|
|
1703
|
-
showEmojiPicker(slug, anchorEl);
|
|
1704
|
-
});
|
|
1705
|
-
menu.appendChild(iconItem);
|
|
1747
|
+
var isWorktree = slug.indexOf("--") !== -1;
|
|
1706
1748
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1749
|
+
if (isWorktree) {
|
|
1750
|
+
// Worktree context menu: only "Remove Worktree"
|
|
1751
|
+
var removeWtItem = document.createElement("button");
|
|
1752
|
+
removeWtItem.className = "project-ctx-item project-ctx-delete";
|
|
1753
|
+
removeWtItem.innerHTML = iconHtml("trash-2") + " <span>Remove Worktree</span>";
|
|
1754
|
+
removeWtItem.addEventListener("click", function (e) {
|
|
1755
|
+
e.stopPropagation();
|
|
1756
|
+
closeProjectCtxMenu();
|
|
1757
|
+
if (ctx.ws && ctx.connected) {
|
|
1758
|
+
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
menu.appendChild(removeWtItem);
|
|
1762
|
+
} else {
|
|
1763
|
+
// Regular project context menu
|
|
1764
|
+
var iconItem = document.createElement("button");
|
|
1765
|
+
iconItem.className = "project-ctx-item";
|
|
1766
|
+
iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
|
|
1767
|
+
iconItem.addEventListener("click", function (e) {
|
|
1768
|
+
e.stopPropagation();
|
|
1769
|
+
closeProjectCtxMenu();
|
|
1770
|
+
showEmojiPicker(slug, anchorEl);
|
|
1771
|
+
});
|
|
1772
|
+
menu.appendChild(iconItem);
|
|
1711
1773
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1774
|
+
// --- Add Worktree ---
|
|
1775
|
+
var wtItem = document.createElement("button");
|
|
1776
|
+
wtItem.className = "project-ctx-item";
|
|
1777
|
+
wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
|
|
1778
|
+
wtItem.addEventListener("click", function (e) {
|
|
1779
|
+
e.stopPropagation();
|
|
1780
|
+
closeProjectCtxMenu();
|
|
1781
|
+
showWorktreeModal(slug, name || slug);
|
|
1782
|
+
});
|
|
1783
|
+
menu.appendChild(wtItem);
|
|
1784
|
+
|
|
1785
|
+
// --- Separator ---
|
|
1786
|
+
var sep = document.createElement("div");
|
|
1787
|
+
sep.className = "project-ctx-separator";
|
|
1788
|
+
menu.appendChild(sep);
|
|
1789
|
+
|
|
1790
|
+
// --- Remove Project ---
|
|
1791
|
+
var removeItem = document.createElement("button");
|
|
1792
|
+
removeItem.className = "project-ctx-item project-ctx-delete";
|
|
1793
|
+
removeItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
|
|
1794
|
+
removeItem.addEventListener("click", function (e) {
|
|
1795
|
+
e.stopPropagation();
|
|
1796
|
+
closeProjectCtxMenu();
|
|
1797
|
+
if (ctx.ws && ctx.connected) {
|
|
1798
|
+
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
menu.appendChild(removeItem);
|
|
1802
|
+
}
|
|
1724
1803
|
|
|
1725
1804
|
document.body.appendChild(menu);
|
|
1726
1805
|
projectCtxMenu = menu;
|
|
@@ -2176,8 +2255,227 @@ export function renderSidebarPresence(onlineUsers) {
|
|
|
2176
2255
|
}
|
|
2177
2256
|
}
|
|
2178
2257
|
|
|
2258
|
+
// --- Worktree folder collapse state (persisted in localStorage) ---
|
|
2259
|
+
var wtCollapsed = {};
|
|
2260
|
+
try {
|
|
2261
|
+
wtCollapsed = JSON.parse(localStorage.getItem("clay-wt-collapsed") || "{}");
|
|
2262
|
+
} catch (e) {}
|
|
2263
|
+
function setWtCollapsed(slug, collapsed) {
|
|
2264
|
+
wtCollapsed[slug] = collapsed;
|
|
2265
|
+
try { localStorage.setItem("clay-wt-collapsed", JSON.stringify(wtCollapsed)); } catch (e) {}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Group projects by parent/worktree relationship
|
|
2269
|
+
function groupProjects(projects) {
|
|
2270
|
+
var parents = [];
|
|
2271
|
+
var wtByParent = {};
|
|
2272
|
+
for (var i = 0; i < projects.length; i++) {
|
|
2273
|
+
var p = projects[i];
|
|
2274
|
+
if (p.isWorktree && p.parentSlug) {
|
|
2275
|
+
if (!wtByParent[p.parentSlug]) wtByParent[p.parentSlug] = [];
|
|
2276
|
+
wtByParent[p.parentSlug].push(p);
|
|
2277
|
+
} else {
|
|
2278
|
+
parents.push(p);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
return { parents: parents, wtByParent: wtByParent };
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// Create a standard icon-strip item element (shared between parent and worktree rendering)
|
|
2285
|
+
function createIconItem(p, currentSlug) {
|
|
2286
|
+
var el = document.createElement("a");
|
|
2287
|
+
var isActive = p.slug === currentSlug && !currentDmUserId;
|
|
2288
|
+
el.className = "icon-strip-item" + (isActive ? " active" : "");
|
|
2289
|
+
el.href = "/p/" + p.slug + "/";
|
|
2290
|
+
el.dataset.slug = p.slug;
|
|
2291
|
+
|
|
2292
|
+
if (p.icon) {
|
|
2293
|
+
var emojiSpan = document.createElement("span");
|
|
2294
|
+
emojiSpan.className = "project-emoji";
|
|
2295
|
+
emojiSpan.textContent = p.icon;
|
|
2296
|
+
parseEmojis(emojiSpan);
|
|
2297
|
+
el.appendChild(emojiSpan);
|
|
2298
|
+
} else {
|
|
2299
|
+
el.appendChild(document.createTextNode(getProjectAbbrev(p.name)));
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
var pill = document.createElement("span");
|
|
2303
|
+
pill.className = "icon-strip-pill";
|
|
2304
|
+
el.appendChild(pill);
|
|
2305
|
+
|
|
2306
|
+
var statusDot = document.createElement("span");
|
|
2307
|
+
statusDot.className = "icon-strip-status";
|
|
2308
|
+
if (p.isProcessing) statusDot.classList.add("processing");
|
|
2309
|
+
el.appendChild(statusDot);
|
|
2310
|
+
|
|
2311
|
+
var projectBadge = document.createElement("span");
|
|
2312
|
+
projectBadge.className = "icon-strip-project-badge";
|
|
2313
|
+
if (p.unread > 0 && !isActive) {
|
|
2314
|
+
projectBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
|
|
2315
|
+
projectBadge.classList.add("has-unread");
|
|
2316
|
+
}
|
|
2317
|
+
el.appendChild(projectBadge);
|
|
2318
|
+
|
|
2319
|
+
(function (name, elem) {
|
|
2320
|
+
elem.addEventListener("mouseenter", function () { showIconTooltip(elem, name); });
|
|
2321
|
+
elem.addEventListener("mouseleave", hideIconTooltip);
|
|
2322
|
+
})(p.name, el);
|
|
2323
|
+
|
|
2324
|
+
(function (slug) {
|
|
2325
|
+
el.addEventListener("click", function (e) {
|
|
2326
|
+
e.preventDefault();
|
|
2327
|
+
if (ctx.switchProject) ctx.switchProject(slug);
|
|
2328
|
+
});
|
|
2329
|
+
})(p.slug);
|
|
2330
|
+
|
|
2331
|
+
return el;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// Worktree creation modal
|
|
2335
|
+
function showWorktreeModal(parentSlug, parentName) {
|
|
2336
|
+
// Remove existing modal if any
|
|
2337
|
+
var existing = document.getElementById("wt-modal-container");
|
|
2338
|
+
if (existing) existing.remove();
|
|
2339
|
+
|
|
2340
|
+
var container = document.createElement("div");
|
|
2341
|
+
container.id = "wt-modal-container";
|
|
2342
|
+
|
|
2343
|
+
var overlay = document.createElement("div");
|
|
2344
|
+
overlay.className = "wt-modal-overlay";
|
|
2345
|
+
container.appendChild(overlay);
|
|
2346
|
+
|
|
2347
|
+
var modal = document.createElement("div");
|
|
2348
|
+
modal.className = "wt-modal";
|
|
2349
|
+
|
|
2350
|
+
var title = document.createElement("div");
|
|
2351
|
+
title.className = "wt-modal-title";
|
|
2352
|
+
title.textContent = "Add Worktree \u2014 " + parentName;
|
|
2353
|
+
modal.appendChild(title);
|
|
2354
|
+
|
|
2355
|
+
var branchLabel = document.createElement("label");
|
|
2356
|
+
branchLabel.className = "wt-modal-label";
|
|
2357
|
+
branchLabel.textContent = "Branch name";
|
|
2358
|
+
modal.appendChild(branchLabel);
|
|
2359
|
+
|
|
2360
|
+
var branchInput = document.createElement("input");
|
|
2361
|
+
branchInput.type = "text";
|
|
2362
|
+
branchInput.className = "wt-modal-input";
|
|
2363
|
+
branchInput.placeholder = "feat/my-feature";
|
|
2364
|
+
branchInput.autocomplete = "off";
|
|
2365
|
+
branchInput.spellcheck = false;
|
|
2366
|
+
modal.appendChild(branchInput);
|
|
2367
|
+
|
|
2368
|
+
var baseLabel = document.createElement("label");
|
|
2369
|
+
baseLabel.className = "wt-modal-label";
|
|
2370
|
+
baseLabel.textContent = "Base branch";
|
|
2371
|
+
modal.appendChild(baseLabel);
|
|
2372
|
+
|
|
2373
|
+
var baseSelect = document.createElement("select");
|
|
2374
|
+
baseSelect.className = "wt-modal-input";
|
|
2375
|
+
// Add "main" as default while loading
|
|
2376
|
+
var defaultOpt = document.createElement("option");
|
|
2377
|
+
defaultOpt.value = "main";
|
|
2378
|
+
defaultOpt.textContent = "main";
|
|
2379
|
+
baseSelect.appendChild(defaultOpt);
|
|
2380
|
+
modal.appendChild(baseSelect);
|
|
2381
|
+
|
|
2382
|
+
// Fetch branches from target project via HTTP API
|
|
2383
|
+
fetch("/p/" + parentSlug + "/api/branches")
|
|
2384
|
+
.then(function (res) { return res.json(); })
|
|
2385
|
+
.then(function (data) {
|
|
2386
|
+
baseSelect.innerHTML = "";
|
|
2387
|
+
var branches = data.branches || ["main"];
|
|
2388
|
+
var defBranch = data.defaultBranch || "main";
|
|
2389
|
+
for (var i = 0; i < branches.length; i++) {
|
|
2390
|
+
var opt = document.createElement("option");
|
|
2391
|
+
opt.value = branches[i];
|
|
2392
|
+
opt.textContent = branches[i];
|
|
2393
|
+
if (branches[i] === defBranch) opt.selected = true;
|
|
2394
|
+
baseSelect.appendChild(opt);
|
|
2395
|
+
}
|
|
2396
|
+
})
|
|
2397
|
+
.catch(function () {});
|
|
2398
|
+
|
|
2399
|
+
var errorDiv = document.createElement("div");
|
|
2400
|
+
errorDiv.className = "wt-modal-error";
|
|
2401
|
+
modal.appendChild(errorDiv);
|
|
2402
|
+
|
|
2403
|
+
var actions = document.createElement("div");
|
|
2404
|
+
actions.className = "wt-modal-actions";
|
|
2405
|
+
|
|
2406
|
+
var cancelBtn = document.createElement("button");
|
|
2407
|
+
cancelBtn.className = "wt-modal-btn";
|
|
2408
|
+
cancelBtn.textContent = "Cancel";
|
|
2409
|
+
actions.appendChild(cancelBtn);
|
|
2410
|
+
|
|
2411
|
+
var createBtn = document.createElement("button");
|
|
2412
|
+
createBtn.className = "wt-modal-btn primary";
|
|
2413
|
+
createBtn.textContent = "Create";
|
|
2414
|
+
actions.appendChild(createBtn);
|
|
2415
|
+
|
|
2416
|
+
modal.appendChild(actions);
|
|
2417
|
+
container.appendChild(modal);
|
|
2418
|
+
document.body.appendChild(container);
|
|
2419
|
+
branchInput.focus();
|
|
2420
|
+
|
|
2421
|
+
function closeModal() { container.remove(); }
|
|
2422
|
+
|
|
2423
|
+
function doCreate() {
|
|
2424
|
+
var branch = branchInput.value.trim();
|
|
2425
|
+
var base = baseSelect.value.trim() || null;
|
|
2426
|
+
if (!branch) {
|
|
2427
|
+
errorDiv.textContent = "Branch name is required";
|
|
2428
|
+
errorDiv.classList.add("visible");
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
// Sanitize: replace slashes with dashes for directory name
|
|
2432
|
+
var dirName = branch.replace(/\//g, "-");
|
|
2433
|
+
createBtn.disabled = true;
|
|
2434
|
+
createBtn.textContent = "Creating...";
|
|
2435
|
+
errorDiv.classList.remove("visible");
|
|
2436
|
+
|
|
2437
|
+
if (ctx.ws && ctx.connected) {
|
|
2438
|
+
ctx.ws.send(JSON.stringify({
|
|
2439
|
+
type: "create_worktree",
|
|
2440
|
+
branch: dirName,
|
|
2441
|
+
baseBranch: base
|
|
2442
|
+
}));
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// Listen for the result
|
|
2446
|
+
var handler = function (event) {
|
|
2447
|
+
var msg;
|
|
2448
|
+
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
2449
|
+
if (msg.type === "create_worktree_result") {
|
|
2450
|
+
ctx.ws.removeEventListener("message", handler);
|
|
2451
|
+
if (msg.ok) {
|
|
2452
|
+
closeModal();
|
|
2453
|
+
if (msg.slug && ctx.switchProject) ctx.switchProject(msg.slug);
|
|
2454
|
+
} else {
|
|
2455
|
+
createBtn.disabled = false;
|
|
2456
|
+
createBtn.textContent = "Create";
|
|
2457
|
+
errorDiv.textContent = msg.error || "Failed to create worktree";
|
|
2458
|
+
errorDiv.classList.add("visible");
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
};
|
|
2462
|
+
ctx.ws.addEventListener("message", handler);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
overlay.addEventListener("click", closeModal);
|
|
2466
|
+
cancelBtn.addEventListener("click", closeModal);
|
|
2467
|
+
createBtn.addEventListener("click", doCreate);
|
|
2468
|
+
branchInput.addEventListener("keydown", function (e) {
|
|
2469
|
+
if (e.key === "Enter") doCreate();
|
|
2470
|
+
if (e.key === "Escape") closeModal();
|
|
2471
|
+
});
|
|
2472
|
+
baseSelect.addEventListener("keydown", function (e) {
|
|
2473
|
+
if (e.key === "Enter") doCreate();
|
|
2474
|
+
if (e.key === "Escape") closeModal();
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2179
2478
|
export function renderIconStrip(projects, currentSlug) {
|
|
2180
|
-
// Cache for mobile sheet
|
|
2181
2479
|
cachedProjectList = projects;
|
|
2182
2480
|
cachedCurrentSlug = currentSlug;
|
|
2183
2481
|
|
|
@@ -2185,71 +2483,154 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
2185
2483
|
if (!container) return;
|
|
2186
2484
|
container.innerHTML = "";
|
|
2187
2485
|
|
|
2188
|
-
|
|
2189
|
-
var p = projects[i];
|
|
2190
|
-
var el = document.createElement("a");
|
|
2191
|
-
var isActive = p.slug === currentSlug && !currentDmUserId;
|
|
2192
|
-
el.className = "icon-strip-item" + (isActive ? " active" : "");
|
|
2193
|
-
el.href = "/p/" + p.slug + "/";
|
|
2194
|
-
el.dataset.slug = p.slug;
|
|
2195
|
-
|
|
2196
|
-
// Icon: twemoji or abbreviation
|
|
2197
|
-
if (p.icon) {
|
|
2198
|
-
var emojiSpan = document.createElement("span");
|
|
2199
|
-
emojiSpan.className = "project-emoji";
|
|
2200
|
-
emojiSpan.textContent = p.icon;
|
|
2201
|
-
parseEmojis(emojiSpan);
|
|
2202
|
-
el.appendChild(emojiSpan);
|
|
2203
|
-
} else {
|
|
2204
|
-
el.appendChild(document.createTextNode(getProjectAbbrev(p.name)));
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
var pill = document.createElement("span");
|
|
2208
|
-
pill.className = "icon-strip-pill";
|
|
2209
|
-
el.appendChild(pill);
|
|
2486
|
+
var grouped = groupProjects(projects);
|
|
2210
2487
|
|
|
2211
|
-
|
|
2212
|
-
var
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
el.appendChild(statusDot);
|
|
2488
|
+
for (var i = 0; i < grouped.parents.length; i++) {
|
|
2489
|
+
var p = grouped.parents[i];
|
|
2490
|
+
var worktrees = grouped.wtByParent[p.slug] || [];
|
|
2491
|
+
var hasWorktrees = worktrees.length > 0;
|
|
2216
2492
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2493
|
+
if (!hasWorktrees) {
|
|
2494
|
+
// Regular project, render as before
|
|
2495
|
+
var el = createIconItem(p, currentSlug);
|
|
2496
|
+
(function (slug, name, elem) {
|
|
2497
|
+
elem.addEventListener("contextmenu", function (e) {
|
|
2498
|
+
e.preventDefault();
|
|
2499
|
+
e.stopPropagation();
|
|
2500
|
+
showIconCtxMenu(elem, slug, name);
|
|
2501
|
+
});
|
|
2502
|
+
})(p.slug, p.name || p.slug, el);
|
|
2503
|
+
setupDragHandlers(el, p.slug);
|
|
2504
|
+
container.appendChild(el);
|
|
2505
|
+
continue;
|
|
2223
2506
|
}
|
|
2224
|
-
el.appendChild(projectBadge);
|
|
2225
2507
|
|
|
2226
|
-
//
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2508
|
+
// Folder group for parent + worktrees
|
|
2509
|
+
var folder = document.createElement("div");
|
|
2510
|
+
folder.className = "icon-strip-group";
|
|
2511
|
+
folder.dataset.parentSlug = p.slug;
|
|
2512
|
+
if (wtCollapsed[p.slug]) folder.classList.add("collapsed");
|
|
2231
2513
|
|
|
2232
|
-
//
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
})(p.slug);
|
|
2514
|
+
// Bubble up worktree processing state to parent
|
|
2515
|
+
if (!p.isProcessing) {
|
|
2516
|
+
for (var wpi = 0; wpi < worktrees.length; wpi++) {
|
|
2517
|
+
if (worktrees[wpi].isProcessing) { p.isProcessing = true; break; }
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2239
2520
|
|
|
2240
|
-
//
|
|
2521
|
+
// Parent icon as folder header
|
|
2522
|
+
var header = createIconItem(p, currentSlug);
|
|
2523
|
+
header.classList.add("folder-header");
|
|
2241
2524
|
(function (slug, name, elem) {
|
|
2242
2525
|
elem.addEventListener("contextmenu", function (e) {
|
|
2243
2526
|
e.preventDefault();
|
|
2244
2527
|
e.stopPropagation();
|
|
2245
2528
|
showIconCtxMenu(elem, slug, name);
|
|
2246
2529
|
});
|
|
2247
|
-
})(p.slug, p.name || p.slug,
|
|
2530
|
+
})(p.slug, p.name || p.slug, header);
|
|
2531
|
+
setupDragHandlers(header, p.slug);
|
|
2532
|
+
|
|
2533
|
+
// Chevron toggle
|
|
2534
|
+
var chevron = document.createElement("span");
|
|
2535
|
+
chevron.className = "icon-strip-group-chevron";
|
|
2536
|
+
chevron.innerHTML = '<i data-lucide="git-branch"></i>';
|
|
2537
|
+
(function (parentSlug, folderEl) {
|
|
2538
|
+
chevron.addEventListener("click", function (e) {
|
|
2539
|
+
e.preventDefault();
|
|
2540
|
+
e.stopPropagation();
|
|
2541
|
+
var nowCollapsed = folderEl.classList.toggle("collapsed");
|
|
2542
|
+
setWtCollapsed(parentSlug, nowCollapsed);
|
|
2543
|
+
});
|
|
2544
|
+
chevron.addEventListener("contextmenu", function (e) {
|
|
2545
|
+
e.preventDefault();
|
|
2546
|
+
e.stopPropagation();
|
|
2547
|
+
});
|
|
2548
|
+
})(p.slug, folder);
|
|
2549
|
+
chevron.setAttribute("data-tip", "Toggle worktrees");
|
|
2550
|
+
header.appendChild(chevron);
|
|
2551
|
+
folder.appendChild(header);
|
|
2552
|
+
|
|
2553
|
+
// Worktree items container
|
|
2554
|
+
var itemsContainer = document.createElement("div");
|
|
2555
|
+
itemsContainer.className = "icon-strip-group-items";
|
|
2556
|
+
|
|
2557
|
+
for (var wi = 0; wi < worktrees.length; wi++) {
|
|
2558
|
+
(function (wt) {
|
|
2559
|
+
var wtEl = document.createElement("a");
|
|
2560
|
+
var isWtActive = wt.slug === currentSlug && !currentDmUserId;
|
|
2561
|
+
var isAccessible = wt.worktreeAccessible !== false;
|
|
2562
|
+
wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (!isAccessible ? " wt-disabled" : "");
|
|
2563
|
+
wtEl.href = "/p/" + wt.slug + "/";
|
|
2564
|
+
wtEl.dataset.slug = wt.slug;
|
|
2565
|
+
|
|
2566
|
+
var abbrev = document.createElement("span");
|
|
2567
|
+
abbrev.className = "wt-branch-abbrev";
|
|
2568
|
+
abbrev.textContent = getProjectAbbrev(wt.name);
|
|
2569
|
+
wtEl.appendChild(abbrev);
|
|
2570
|
+
|
|
2571
|
+
var wtStatus = document.createElement("span");
|
|
2572
|
+
wtStatus.className = "icon-strip-status";
|
|
2573
|
+
if (wt.isProcessing) wtStatus.classList.add("processing");
|
|
2574
|
+
wtEl.appendChild(wtStatus);
|
|
2575
|
+
|
|
2576
|
+
var tooltipText = wt.name;
|
|
2577
|
+
if (!isAccessible) {
|
|
2578
|
+
tooltipText += " (outside project path, cannot be accessed)";
|
|
2579
|
+
}
|
|
2580
|
+
(function (text, elem) {
|
|
2581
|
+
elem.addEventListener("mouseenter", function () { showIconTooltip(elem, text); });
|
|
2582
|
+
elem.addEventListener("mouseleave", hideIconTooltip);
|
|
2583
|
+
})(tooltipText, wtEl);
|
|
2584
|
+
|
|
2585
|
+
if (isAccessible) {
|
|
2586
|
+
(function (slug) {
|
|
2587
|
+
wtEl.addEventListener("click", function (e) {
|
|
2588
|
+
e.preventDefault();
|
|
2589
|
+
if (ctx.switchProject) ctx.switchProject(slug);
|
|
2590
|
+
});
|
|
2591
|
+
})(wt.slug);
|
|
2592
|
+
} else {
|
|
2593
|
+
wtEl.addEventListener("click", function (e) {
|
|
2594
|
+
e.preventDefault();
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2248
2597
|
|
|
2249
|
-
|
|
2250
|
-
|
|
2598
|
+
if (isAccessible) {
|
|
2599
|
+
(function (slug, name, elem) {
|
|
2600
|
+
elem.addEventListener("contextmenu", function (e) {
|
|
2601
|
+
e.preventDefault();
|
|
2602
|
+
e.stopPropagation();
|
|
2603
|
+
showIconCtxMenu(elem, slug, name);
|
|
2604
|
+
});
|
|
2605
|
+
})(wt.slug, wt.name, wtEl);
|
|
2606
|
+
} else {
|
|
2607
|
+
wtEl.addEventListener("contextmenu", function (e) {
|
|
2608
|
+
e.preventDefault();
|
|
2609
|
+
e.stopPropagation();
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
itemsContainer.appendChild(wtEl);
|
|
2614
|
+
})(worktrees[wi]);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// "+" button to add new worktree
|
|
2618
|
+
var addBtn = document.createElement("button");
|
|
2619
|
+
addBtn.className = "icon-strip-group-add";
|
|
2620
|
+
addBtn.textContent = "+";
|
|
2621
|
+
(function (parentSlug, parentName, btn) {
|
|
2622
|
+
btn.addEventListener("click", function (e) {
|
|
2623
|
+
e.preventDefault();
|
|
2624
|
+
e.stopPropagation();
|
|
2625
|
+
showWorktreeModal(parentSlug, parentName);
|
|
2626
|
+
});
|
|
2627
|
+
btn.addEventListener("mouseenter", function () { showIconTooltip(btn, "New worktree"); });
|
|
2628
|
+
btn.addEventListener("mouseleave", hideIconTooltip);
|
|
2629
|
+
})(p.slug, p.name, addBtn);
|
|
2630
|
+
itemsContainer.appendChild(addBtn);
|
|
2251
2631
|
|
|
2252
|
-
|
|
2632
|
+
folder.appendChild(itemsContainer);
|
|
2633
|
+
container.appendChild(folder);
|
|
2253
2634
|
}
|
|
2254
2635
|
|
|
2255
2636
|
// Update home icon active state
|
|
@@ -2262,8 +2643,10 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
2262
2643
|
}
|
|
2263
2644
|
}
|
|
2264
2645
|
|
|
2265
|
-
// Also update mobile project list
|
|
2266
2646
|
renderProjectList(projects, currentSlug);
|
|
2647
|
+
|
|
2648
|
+
// Render Lucide icons added dynamically (e.g. worktree git-branch icon)
|
|
2649
|
+
try { lucide.createIcons({ nodes: [container] }); } catch (e) {}
|
|
2267
2650
|
}
|
|
2268
2651
|
|
|
2269
2652
|
function renderProjectList(projects, currentSlug) {
|
|
@@ -2271,35 +2654,80 @@ function renderProjectList(projects, currentSlug) {
|
|
|
2271
2654
|
if (!list) return;
|
|
2272
2655
|
list.innerHTML = "";
|
|
2273
2656
|
|
|
2274
|
-
|
|
2275
|
-
(function (p) {
|
|
2276
|
-
var el = document.createElement("button");
|
|
2277
|
-
el.className = "mobile-project-item" + (p.slug === currentSlug ? " active" : "");
|
|
2657
|
+
var grouped = groupProjects(projects);
|
|
2278
2658
|
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
el.appendChild(abbrev);
|
|
2659
|
+
for (var i = 0; i < grouped.parents.length; i++) {
|
|
2660
|
+
var p = grouped.parents[i];
|
|
2661
|
+
var worktrees = grouped.wtByParent[p.slug] || [];
|
|
2283
2662
|
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2663
|
+
if (worktrees.length === 0) {
|
|
2664
|
+
// Regular project
|
|
2665
|
+
list.appendChild(createMobileProjectItem(p, currentSlug, false));
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2288
2668
|
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2669
|
+
// Folder for parent + worktrees
|
|
2670
|
+
var folderDiv = document.createElement("div");
|
|
2671
|
+
folderDiv.className = "mobile-project-folder";
|
|
2672
|
+
if (wtCollapsed[p.slug]) folderDiv.classList.add("collapsed");
|
|
2673
|
+
|
|
2674
|
+
var headerEl = createMobileProjectItem(p, currentSlug, false);
|
|
2675
|
+
var chevron = document.createElement("span");
|
|
2676
|
+
chevron.className = "mobile-folder-chevron";
|
|
2677
|
+
chevron.innerHTML = "▼";
|
|
2678
|
+
(function (parentSlug, fDiv) {
|
|
2679
|
+
chevron.addEventListener("click", function (e) {
|
|
2680
|
+
e.preventDefault();
|
|
2681
|
+
e.stopPropagation();
|
|
2682
|
+
var nowCollapsed = fDiv.classList.toggle("collapsed");
|
|
2683
|
+
setWtCollapsed(parentSlug, nowCollapsed);
|
|
2684
|
+
});
|
|
2685
|
+
})(p.slug, folderDiv);
|
|
2686
|
+
headerEl.appendChild(chevron);
|
|
2687
|
+
folderDiv.appendChild(headerEl);
|
|
2688
|
+
|
|
2689
|
+
var wtList = document.createElement("div");
|
|
2690
|
+
wtList.className = "mobile-folder-items";
|
|
2691
|
+
for (var wi = 0; wi < worktrees.length; wi++) {
|
|
2692
|
+
var isAccessible = worktrees[wi].worktreeAccessible !== false;
|
|
2693
|
+
var wtItem = createMobileProjectItem(worktrees[wi], currentSlug, true);
|
|
2694
|
+
if (!isAccessible) wtItem.classList.add("wt-disabled");
|
|
2695
|
+
if (!isAccessible) {
|
|
2696
|
+
wtItem.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); });
|
|
2293
2697
|
}
|
|
2698
|
+
wtList.appendChild(wtItem);
|
|
2699
|
+
}
|
|
2700
|
+
folderDiv.appendChild(wtList);
|
|
2701
|
+
list.appendChild(folderDiv);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2294
2704
|
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2705
|
+
function createMobileProjectItem(p, currentSlug, isWorktree) {
|
|
2706
|
+
var el = document.createElement("button");
|
|
2707
|
+
el.className = "mobile-project-item" + (p.slug === currentSlug ? " active" : "") + (isWorktree ? " wt-item" : "");
|
|
2708
|
+
|
|
2709
|
+
var abbrev = document.createElement("span");
|
|
2710
|
+
abbrev.className = "mobile-project-abbrev";
|
|
2711
|
+
abbrev.textContent = getProjectAbbrev(p.name);
|
|
2712
|
+
el.appendChild(abbrev);
|
|
2299
2713
|
|
|
2300
|
-
|
|
2301
|
-
|
|
2714
|
+
var name = document.createElement("span");
|
|
2715
|
+
name.className = "mobile-project-name";
|
|
2716
|
+
name.textContent = p.name;
|
|
2717
|
+
el.appendChild(name);
|
|
2718
|
+
|
|
2719
|
+
if (p.isProcessing) {
|
|
2720
|
+
var dot = document.createElement("span");
|
|
2721
|
+
dot.className = "mobile-project-processing";
|
|
2722
|
+
el.appendChild(dot);
|
|
2302
2723
|
}
|
|
2724
|
+
|
|
2725
|
+
el.addEventListener("click", function () {
|
|
2726
|
+
if (ctx.switchProject) ctx.switchProject(p.slug);
|
|
2727
|
+
closeSidebar();
|
|
2728
|
+
});
|
|
2729
|
+
|
|
2730
|
+
return el;
|
|
2303
2731
|
}
|
|
2304
2732
|
|
|
2305
2733
|
export function getEmojiCategories() { return EMOJI_CATEGORIES; }
|