clay-server 2.47.0-beta.5 → 3.0.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.
Files changed (58) hide show
  1. package/lib/daemon.js +6 -27
  2. package/lib/project-connection.js +21 -8
  3. package/lib/project-http.js +5 -2
  4. package/lib/project-session-pair.js +312 -0
  5. package/lib/project-session-spawn.js +8 -4
  6. package/lib/project-sessions.js +108 -10
  7. package/lib/project-user-message.js +18 -8
  8. package/lib/project.js +33 -0
  9. package/lib/public/app.js +24 -1
  10. package/lib/public/css/icon-strip.css +0 -190
  11. package/lib/public/css/menus.css +83 -1
  12. package/lib/public/css/messages.css +14 -0
  13. package/lib/public/css/mobile-nav.css +4 -0
  14. package/lib/public/css/pane.css +420 -0
  15. package/lib/public/css/sidebar.css +40 -0
  16. package/lib/public/css/sticky-notes.css +2 -0
  17. package/lib/public/css/title-bar.css +169 -0
  18. package/lib/public/index.html +10 -0
  19. package/lib/public/modules/app-connection.js +6 -0
  20. package/lib/public/modules/app-favicon.js +6 -10
  21. package/lib/public/modules/app-header.js +101 -6
  22. package/lib/public/modules/app-home-hub.js +2 -0
  23. package/lib/public/modules/app-messages.js +63 -13
  24. package/lib/public/modules/app-panels.js +49 -27
  25. package/lib/public/modules/app-projects.js +43 -30
  26. package/lib/public/modules/app-rendering.js +11 -4
  27. package/lib/public/modules/branch-switcher.js +178 -0
  28. package/lib/public/modules/dom-refs.js +0 -1
  29. package/lib/public/modules/pane-bridge.js +41 -0
  30. package/lib/public/modules/pane-session.js +18 -0
  31. package/lib/public/modules/project-switcher.js +2 -1
  32. package/lib/public/modules/sidebar-mobile.js +19 -6
  33. package/lib/public/modules/sidebar-projects.js +63 -236
  34. package/lib/public/modules/sidebar-sessions.js +175 -1
  35. package/lib/public/modules/sidebar.js +2 -0
  36. package/lib/public/modules/split-group-helpers.js +18 -0
  37. package/lib/public/modules/split-pair-ui.js +324 -0
  38. package/lib/public/modules/split-view.js +499 -0
  39. package/lib/public/modules/sticky-notes.js +32 -91
  40. package/lib/public/modules/worktree-family.js +60 -0
  41. package/lib/public/style.css +1 -0
  42. package/lib/sdk-bridge.js +111 -20
  43. package/lib/sdk-message-processor.js +2 -2
  44. package/lib/server.js +24 -4
  45. package/lib/session-hygiene.js +100 -0
  46. package/lib/session-pair-mcp-server.js +28 -0
  47. package/lib/session-split-groups.js +298 -0
  48. package/lib/sessions.js +163 -34
  49. package/lib/ws-request.js +14 -0
  50. package/lib/ws-schema.js +13 -0
  51. package/lib/yoke/adapters/claude.js +32 -0
  52. package/lib/yoke/adapters/codex.js +100 -77
  53. package/lib/yoke/adapters/kiro.js +34 -35
  54. package/lib/yoke/codex-app-server.js +56 -6
  55. package/lib/yoke/index.js +1 -0
  56. package/lib/yoke/skill-discovery.js +196 -0
  57. package/lib/yoke/vendor-registry.js +34 -0
  58. package/package.json +1 -1
@@ -10,9 +10,10 @@ import { store } from './store.js';
10
10
  import { getWs } from './ws-ref.js';
11
11
  import { closeSidebar } from './sidebar.js';
12
12
  import { showIconTooltip, hideIconTooltip, closeUserCtxMenu, getCurrentDmUserId } from './sidebar-mates.js';
13
- import { switchProject, openAddProjectModal, getCachedProjects } from './app-projects.js';
13
+ import { switchProject, openAddProjectModal } from './app-projects.js';
14
14
  import { showHomeHub } from './app-home-hub.js';
15
15
  import { getPendingTuiAttention } from './app-notifications.js';
16
+ import { aggregateFamily } from './worktree-family.js';
16
17
 
17
18
  // --- Project state ---
18
19
  var cachedProjectList = [];
@@ -31,12 +32,6 @@ var emojiPickerEl = null;
31
32
  var draggedSlug = null;
32
33
  var draggedEl = null;
33
34
 
34
- // --- Worktree folder collapse state (persisted in localStorage) ---
35
- var wtCollapsed = {};
36
- try {
37
- wtCollapsed = JSON.parse(localStorage.getItem("clay-wt-collapsed") || "{}");
38
- } catch (e) {}
39
-
40
35
  var EMOJI_CATEGORIES = [
41
36
  { id: "frequent", icon: "🕐", label: "Frequent", emojis: [
42
37
  "😀","😎","🤓","🧠","💡","🔥","⚡","🚀",
@@ -524,17 +519,8 @@ function showIconCtxMenu(anchorEl, slug, name) {
524
519
  }
525
520
  });
526
521
  menu.appendChild(removeWtItem);
527
- } else {
528
- var wtItem = document.createElement("button");
529
- wtItem.className = "project-ctx-item";
530
- wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
531
- wtItem.addEventListener("click", function (e) {
532
- e.stopPropagation();
533
- closeProjectCtxMenu();
534
- showWorktreeModal(slug, name || slug);
535
- });
536
- menu.appendChild(wtItem);
537
522
  }
523
+ // Worktree creation lives in the title-bar branch chip, not here.
538
524
 
539
525
  document.body.appendChild(menu);
540
526
  projectCtxMenu = menu;
@@ -619,20 +605,7 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
619
605
  }
620
606
  }
621
607
 
622
- var sep2 = document.createElement("div");
623
- sep2.className = "project-ctx-separator";
624
- menu.appendChild(sep2);
625
-
626
- // --- Add Worktree ---
627
- var wtItem = document.createElement("button");
628
- wtItem.className = "project-ctx-item";
629
- wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
630
- wtItem.addEventListener("click", function (e) {
631
- e.stopPropagation();
632
- closeProjectCtxMenu();
633
- showWorktreeModal(slug, name || slug);
634
- });
635
- menu.appendChild(wtItem);
608
+ // Worktree creation lives in the title-bar branch chip, not here.
636
609
 
637
610
  if (!store.get('permissions') || store.get('permissions').deleteProject !== false) {
638
611
  var sep3 = document.createElement("div");
@@ -953,13 +926,6 @@ function setupDragHandlers(el, slug) {
953
926
  });
954
927
  }
955
928
 
956
- // --- Worktree folder collapse ---
957
-
958
- function setWtCollapsed(slug, collapsed) {
959
- wtCollapsed[slug] = collapsed;
960
- try { localStorage.setItem("clay-wt-collapsed", JSON.stringify(wtCollapsed)); } catch (e) {}
961
- }
962
-
963
929
  function groupProjects(projects) {
964
930
  var parents = [];
965
931
  var wtByParent = {};
@@ -975,6 +941,21 @@ function groupProjects(projects) {
975
941
  return { parents: parents, wtByParent: wtByParent };
976
942
  }
977
943
 
944
+ function aggregateProjectFamily(parent, worktrees) {
945
+ var aggregate = aggregateFamily(parent, worktrees);
946
+ aggregate.hasFamilyPendingAttention = getPendingTuiAttention(parent.slug) > 0;
947
+ aggregate.hasWorktreeUnread = false;
948
+ aggregate.hasWorktreePending = false;
949
+ for (var i = 0; i < worktrees.length; i++) {
950
+ if (getPendingTuiAttention(worktrees[i].slug) > 0) aggregate.hasFamilyPendingAttention = true;
951
+ if (worktrees[i].unread > 0) aggregate.hasWorktreeUnread = true;
952
+ if (worktrees[i].pendingPermissions > 0 || getPendingTuiAttention(worktrees[i].slug) > 0) {
953
+ aggregate.hasWorktreePending = true;
954
+ }
955
+ }
956
+ return aggregate;
957
+ }
958
+
978
959
  // --- Icon item creation ---
979
960
 
980
961
  function createIconItem(p, currentSlug) {
@@ -1006,7 +987,7 @@ function createIconItem(p, currentSlug) {
1006
987
 
1007
988
  var projectBadge = document.createElement("span");
1008
989
  projectBadge.className = "icon-strip-project-badge";
1009
- if (p.unread > 0 && !isActive) {
990
+ if (p.unread > 0 && (!isActive || p.hasWorktreeUnread)) {
1010
991
  projectBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
1011
992
  projectBadge.classList.add("has-unread");
1012
993
  }
@@ -1016,7 +997,8 @@ function createIconItem(p, currentSlug) {
1016
997
  // broadcast — they surface via tui_attention notifications. We shake the
1017
998
  // same icon for either, so the user notices regardless of which engine
1018
999
  // is asking.
1019
- if (!isActive && (p.pendingPermissions > 0 || getPendingTuiAttention(p.slug) > 0)) {
1000
+ if ((!isActive && (p.pendingPermissions > 0 || p.hasFamilyPendingAttention || getPendingTuiAttention(p.slug) > 0)) ||
1001
+ p.hasWorktreePending) {
1020
1002
  el.classList.add("has-pending-perm");
1021
1003
  }
1022
1004
 
@@ -1037,7 +1019,7 @@ function createIconItem(p, currentSlug) {
1037
1019
 
1038
1020
  // --- Worktree creation modal ---
1039
1021
 
1040
- function showWorktreeModal(parentSlug, parentName) {
1022
+ export function showWorktreeModal(parentSlug, parentName) {
1041
1023
  var existing = document.getElementById("wt-modal-container");
1042
1024
  if (existing) existing.remove();
1043
1025
 
@@ -1192,164 +1174,17 @@ export function renderIconStrip(projects, currentSlug) {
1192
1174
  for (var i = 0; i < grouped.parents.length; i++) {
1193
1175
  var p = grouped.parents[i];
1194
1176
  var worktrees = grouped.wtByParent[p.slug] || [];
1195
- var hasWorktrees = worktrees.length > 0;
1196
-
1197
- if (!hasWorktrees) {
1198
- var el = createIconItem(p, currentSlug);
1199
- (function (slug, name, elem) {
1200
- elem.addEventListener("contextmenu", function (e) {
1201
- e.preventDefault();
1202
- e.stopPropagation();
1203
- showIconCtxMenu(elem, slug, name);
1204
- });
1205
- })(p.slug, p.name || p.slug, el);
1206
- setupDragHandlers(el, p.slug);
1207
- container.appendChild(el);
1208
- continue;
1209
- }
1210
-
1211
- // Folder group for parent + worktrees
1212
- var folder = document.createElement("div");
1213
- folder.className = "icon-strip-group";
1214
- folder.dataset.parentSlug = p.slug;
1215
- if (wtCollapsed[p.slug]) folder.classList.add("collapsed");
1216
-
1217
- if (!p.isProcessing) {
1218
- for (var wpi = 0; wpi < worktrees.length; wpi++) {
1219
- if (worktrees[wpi].isProcessing) { p.isProcessing = true; break; }
1220
- }
1221
- }
1222
-
1223
- var header = createIconItem(p, currentSlug);
1224
- header.classList.add("folder-header");
1177
+ var familyProject = aggregateProjectFamily(p, worktrees);
1178
+ var el = createIconItem(familyProject, currentSlug);
1225
1179
  (function (slug, name, elem) {
1226
1180
  elem.addEventListener("contextmenu", function (e) {
1227
1181
  e.preventDefault();
1228
1182
  e.stopPropagation();
1229
1183
  showIconCtxMenu(elem, slug, name);
1230
1184
  });
1231
- })(p.slug, p.name || p.slug, header);
1232
- setupDragHandlers(header, p.slug);
1233
-
1234
- var chevron = document.createElement("span");
1235
- chevron.className = "icon-strip-group-chevron";
1236
- chevron.innerHTML = '<i data-lucide="git-branch"></i>';
1237
- (function (parentSlug, folderEl) {
1238
- chevron.addEventListener("click", function (e) {
1239
- e.preventDefault();
1240
- e.stopPropagation();
1241
- var nowCollapsed = folderEl.classList.toggle("collapsed");
1242
- setWtCollapsed(parentSlug, nowCollapsed);
1243
- });
1244
- chevron.addEventListener("contextmenu", function (e) {
1245
- e.preventDefault();
1246
- e.stopPropagation();
1247
- });
1248
- })(p.slug, folder);
1249
- chevron.setAttribute("data-tip", "Toggle worktrees");
1250
- header.appendChild(chevron);
1251
- folder.appendChild(header);
1252
-
1253
- var itemsContainer = document.createElement("div");
1254
- itemsContainer.className = "icon-strip-group-items";
1255
-
1256
- for (var wi = 0; wi < worktrees.length; wi++) {
1257
- (function (wt) {
1258
- var wtEl = document.createElement("a");
1259
- var isWtActive = wt.slug === currentSlug && !currentDmUserId;
1260
- var isAccessible = wt.worktreeAccessible !== false;
1261
- wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (!isAccessible ? " wt-disabled" : "");
1262
- wtEl.href = "/p/" + wt.slug + "/";
1263
- wtEl.dataset.slug = wt.slug;
1264
-
1265
- if (wt.icon) {
1266
- var wtEmoji = document.createElement("span");
1267
- wtEmoji.className = "wt-branch-abbrev project-emoji";
1268
- wtEmoji.textContent = wt.icon;
1269
- parseEmojis(wtEmoji);
1270
- wtEl.appendChild(wtEmoji);
1271
- } else {
1272
- var abbrev = document.createElement("span");
1273
- abbrev.className = "wt-branch-abbrev";
1274
- abbrev.textContent = getProjectAbbrev(wt.name);
1275
- wtEl.appendChild(abbrev);
1276
- }
1277
-
1278
- var wtStatus = document.createElement("span");
1279
- wtStatus.className = "icon-strip-status";
1280
- if (wt.isProcessing) wtStatus.classList.add("processing");
1281
- wtEl.appendChild(wtStatus);
1282
-
1283
- var tooltipText = wt.name;
1284
- if (!isAccessible) {
1285
- tooltipText += " (outside project path, cannot be accessed)";
1286
- }
1287
- (function (text, elem) {
1288
- elem.addEventListener("mouseenter", function () { showIconTooltip(elem, text); });
1289
- elem.addEventListener("mouseleave", hideIconTooltip);
1290
- })(tooltipText, wtEl);
1291
-
1292
- if (isAccessible) {
1293
- (function (slug) {
1294
- wtEl.addEventListener("click", function (e) {
1295
- e.preventDefault();
1296
- if (switchProject) switchProject(slug);
1297
- });
1298
- })(wt.slug);
1299
- } else {
1300
- wtEl.addEventListener("click", function (e) {
1301
- e.preventDefault();
1302
- });
1303
- }
1304
-
1305
- if (isAccessible) {
1306
- (function (slug, name, elem) {
1307
- elem.addEventListener("contextmenu", function (e) {
1308
- e.preventDefault();
1309
- e.stopPropagation();
1310
- showIconCtxMenu(elem, slug, name);
1311
- });
1312
- })(wt.slug, wt.name, wtEl);
1313
- } else {
1314
- wtEl.addEventListener("contextmenu", function (e) {
1315
- e.preventDefault();
1316
- e.stopPropagation();
1317
- });
1318
- }
1319
-
1320
- if (!isWtActive && (wt.pendingPermissions > 0 || getPendingTuiAttention(wt.slug) > 0)) {
1321
- wtEl.classList.add("has-pending-perm");
1322
- }
1323
-
1324
- itemsContainer.appendChild(wtEl);
1325
- })(worktrees[wi]);
1326
- }
1327
-
1328
- var hasWtPendingPerm = false;
1329
- for (var wpi2 = 0; wpi2 < worktrees.length; wpi2++) {
1330
- if (worktrees[wpi2].pendingPermissions > 0 || getPendingTuiAttention(worktrees[wpi2].slug) > 0) {
1331
- hasWtPendingPerm = true;
1332
- break;
1333
- }
1334
- }
1335
- if (hasWtPendingPerm) folder.classList.remove("collapsed");
1336
-
1337
- var addBtn = document.createElement("button");
1338
- addBtn.className = "icon-strip-group-add";
1339
- addBtn.textContent = "+";
1340
- (function (parentSlug, parentName, btn) {
1341
- btn.addEventListener("click", function (e) {
1342
- e.preventDefault();
1343
- e.stopPropagation();
1344
- showWorktreeModal(parentSlug, parentName);
1345
- });
1346
- btn.addEventListener("mouseenter", function () { showIconTooltip(btn, "New worktree"); });
1347
- btn.addEventListener("mouseleave", hideIconTooltip);
1348
- })(p.slug, p.name, addBtn);
1349
- itemsContainer.appendChild(addBtn);
1350
-
1351
- folder.appendChild(itemsContainer);
1352
- container.appendChild(folder);
1185
+ })(p.slug, p.name || p.slug, el);
1186
+ setupDragHandlers(el, p.slug);
1187
+ container.appendChild(el);
1353
1188
  }
1354
1189
 
1355
1190
  // Update home icon active state
@@ -1377,50 +1212,17 @@ function renderProjectList(projects, currentSlug) {
1377
1212
  for (var i = 0; i < grouped.parents.length; i++) {
1378
1213
  var p = grouped.parents[i];
1379
1214
  var worktrees = grouped.wtByParent[p.slug] || [];
1380
-
1381
- if (worktrees.length === 0) {
1382
- list.appendChild(createMobileProjectItem(p, currentSlug, false));
1383
- continue;
1384
- }
1385
-
1386
- var folderDiv = document.createElement("div");
1387
- folderDiv.className = "mobile-project-folder";
1388
- if (wtCollapsed[p.slug]) folderDiv.classList.add("collapsed");
1389
-
1390
- var headerEl = createMobileProjectItem(p, currentSlug, false);
1391
- var chevron = document.createElement("span");
1392
- chevron.className = "mobile-folder-chevron";
1393
- chevron.innerHTML = "&#9660;";
1394
- (function (parentSlug, fDiv) {
1395
- chevron.addEventListener("click", function (e) {
1396
- e.preventDefault();
1397
- e.stopPropagation();
1398
- var nowCollapsed = fDiv.classList.toggle("collapsed");
1399
- setWtCollapsed(parentSlug, nowCollapsed);
1400
- });
1401
- })(p.slug, folderDiv);
1402
- headerEl.appendChild(chevron);
1403
- folderDiv.appendChild(headerEl);
1404
-
1405
- var wtList = document.createElement("div");
1406
- wtList.className = "mobile-folder-items";
1407
- for (var wi = 0; wi < worktrees.length; wi++) {
1408
- var isAccessible = worktrees[wi].worktreeAccessible !== false;
1409
- var wtItem = createMobileProjectItem(worktrees[wi], currentSlug, true);
1410
- if (!isAccessible) wtItem.classList.add("wt-disabled");
1411
- if (!isAccessible) {
1412
- wtItem.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); });
1413
- }
1414
- wtList.appendChild(wtItem);
1415
- }
1416
- folderDiv.appendChild(wtList);
1417
- list.appendChild(folderDiv);
1215
+ list.appendChild(createMobileProjectItem(aggregateProjectFamily(p, worktrees), currentSlug));
1418
1216
  }
1419
1217
  }
1420
1218
 
1421
- function createMobileProjectItem(p, currentSlug, isWorktree) {
1219
+ function createMobileProjectItem(p, currentSlug) {
1422
1220
  var el = document.createElement("button");
1423
- el.className = "mobile-project-item" + (p.slug === currentSlug ? " active" : "") + (isWorktree ? " wt-item" : "");
1221
+ var isActive = p.slug === currentSlug;
1222
+ el.className = "mobile-project-item" + (isActive ? " active" : "");
1223
+ if ((!isActive && (p.pendingPermissions > 0 || p.hasFamilyPendingAttention)) || p.hasWorktreePending) {
1224
+ el.classList.add("has-pending-perm");
1225
+ }
1424
1226
 
1425
1227
  var abbrev = document.createElement("span");
1426
1228
  abbrev.className = "mobile-project-abbrev";
@@ -1443,6 +1245,13 @@ function createMobileProjectItem(p, currentSlug, isWorktree) {
1443
1245
  el.appendChild(dot);
1444
1246
  }
1445
1247
 
1248
+ if (p.unread > 0 && (!isActive || p.hasWorktreeUnread)) {
1249
+ var badge = document.createElement("span");
1250
+ badge.className = "mobile-project-unread";
1251
+ badge.textContent = p.unread > 99 ? "99+" : String(p.unread);
1252
+ el.appendChild(badge);
1253
+ }
1254
+
1446
1255
  el.addEventListener("click", function () {
1447
1256
  if (switchProject) switchProject(p.slug);
1448
1257
  if (closeSidebar) closeSidebar();
@@ -1454,12 +1263,30 @@ function createMobileProjectItem(p, currentSlug, isWorktree) {
1454
1263
  export function getEmojiCategories() { return EMOJI_CATEGORIES; }
1455
1264
 
1456
1265
  export function updateProjectBadge(slug, count) {
1457
- var icon = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
1266
+ var targetSlug = slug;
1267
+ for (var i = 0; i < cachedProjectList.length; i++) {
1268
+ if (cachedProjectList[i].slug !== slug) continue;
1269
+ cachedProjectList[i].unread = count;
1270
+ if (cachedProjectList[i].isWorktree && cachedProjectList[i].parentSlug) {
1271
+ targetSlug = cachedProjectList[i].parentSlug;
1272
+ }
1273
+ break;
1274
+ }
1275
+
1276
+ var displayCount = count;
1277
+ var grouped = groupProjects(cachedProjectList);
1278
+ for (var pi = 0; pi < grouped.parents.length; pi++) {
1279
+ if (grouped.parents[pi].slug !== targetSlug) continue;
1280
+ displayCount = aggregateFamily(grouped.parents[pi], grouped.wtByParent[targetSlug] || []).unread;
1281
+ break;
1282
+ }
1283
+
1284
+ var icon = document.querySelector('.icon-strip-item[data-slug="' + targetSlug + '"]');
1458
1285
  if (!icon) return;
1459
1286
  var badge = icon.querySelector(".icon-strip-project-badge");
1460
1287
  if (!badge) return;
1461
- if (count > 0) {
1462
- badge.textContent = count > 99 ? "99+" : String(count);
1288
+ if (displayCount > 0) {
1289
+ badge.textContent = displayCount > 99 ? "99+" : String(displayCount);
1463
1290
  badge.classList.add("has-unread");
1464
1291
  } else {
1465
1292
  badge.textContent = "";
@@ -13,6 +13,9 @@ import { showConfirm } from './app-misc.js';
13
13
  import { getUpcomingSchedules } from './scheduler.js';
14
14
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
15
15
  import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
16
+ import { openGroup, separateGroup } from './split-view.js';
17
+ import { groupedSessionIds } from './split-group-helpers.js';
18
+ import { openPairDialog } from './split-pair-ui.js';
16
19
 
17
20
 
18
21
  // --- Session state ---
@@ -230,6 +233,9 @@ function collectItemSessionIds(item) {
230
233
  }
231
234
  return ids;
232
235
  }
236
+ if (item.type === "split-group" && Array.isArray(item.members)) {
237
+ return item.members.map(function (session) { return session.id; });
238
+ }
233
239
  return [];
234
240
  }
235
241
 
@@ -407,6 +413,23 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
407
413
  })(VENDOR_ORDER[i]);
408
414
  }
409
415
 
416
+ if (installed.length > 0) {
417
+ var pairSep = document.createElement("div");
418
+ pairSep.className = "session-ctx-sep";
419
+ menu.appendChild(pairSep);
420
+ var pairItem = document.createElement("button");
421
+ pairItem.className = "session-ctx-item";
422
+ pairItem.type = "button";
423
+ pairItem.innerHTML = iconHtml("panel-left-close") + " <span>Pair session…</span>";
424
+ pairItem.title = "Create a Driver and Worker in a split view";
425
+ pairItem.addEventListener("click", function (e) {
426
+ e.stopPropagation();
427
+ closeSessionCtxMenu();
428
+ openPairDialog();
429
+ });
430
+ menu.appendChild(pairItem);
431
+ }
432
+
410
433
  // Claude-only launch mode: a bare TUI shell with permission prompts off.
411
434
  // Hidden in GUI mode (the default) because the item only ever produces a
412
435
  // terminal session, which contradicts what the rest of the menu creates.
@@ -418,7 +441,7 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
418
441
  var skipItem = document.createElement("button");
419
442
  skipItem.className = "session-ctx-item";
420
443
  skipItem.type = "button";
421
- skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip permissions (TUI)</span>";
444
+ skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip Permissions (TUI)</span>";
422
445
  skipItem.title = "Start a terminal session with --dangerously-skip-permissions";
423
446
  skipItem.addEventListener("click", function (e) {
424
447
  e.stopPropagation();
@@ -590,6 +613,7 @@ export function initSidebarSessions() {
590
613
  if (state.lastVendor !== prev.lastVendor || state.installedVendors !== prev.installedVendors) {
591
614
  renderSessionList(null);
592
615
  }
616
+ if (state.splitPanes !== prev.splitPanes) renderSessionList(null);
593
617
  });
594
618
 
595
619
  // --- Resume session picker ---
@@ -656,6 +680,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
656
680
  });
657
681
  menu.appendChild(renameItem);
658
682
 
683
+
659
684
  // Session visibility toggle (only the session owner can change)
660
685
  if (store.get('isMultiUserMode') && sessionData && sessionData.ownerId && sessionData.ownerId === store.get('myUserId')) {
661
686
  var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
@@ -705,6 +730,45 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
705
730
  });
706
731
  }
707
732
 
733
+ function showSplitGroupCtxMenu(anchor, group) {
734
+ closeSessionCtxMenu();
735
+ var menu = document.createElement("div");
736
+ menu.className = "session-ctx-menu";
737
+
738
+ var renameItem = document.createElement("button");
739
+ renameItem.className = "session-ctx-item";
740
+ renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
741
+ renameItem.addEventListener("click", function (e) {
742
+ e.stopPropagation();
743
+ closeSessionCtxMenu();
744
+ startSplitGroupInlineRename(group);
745
+ });
746
+ menu.appendChild(renameItem);
747
+
748
+ var separateItem = document.createElement("button");
749
+ separateItem.className = "session-ctx-item";
750
+ separateItem.innerHTML = iconHtml("columns-2") + " <span>Separate</span>";
751
+ separateItem.addEventListener("click", function (e) {
752
+ e.stopPropagation();
753
+ closeSessionCtxMenu();
754
+ separateGroup(group);
755
+ });
756
+ menu.appendChild(separateItem);
757
+
758
+ document.body.appendChild(menu);
759
+ sessionCtxMenu = menu;
760
+ refreshIcons();
761
+ requestAnimationFrame(function () {
762
+ var rect = anchor.getBoundingClientRect();
763
+ menu.style.position = "fixed";
764
+ menu.style.top = (rect.bottom + 2) + "px";
765
+ menu.style.right = (window.innerWidth - rect.right) + "px";
766
+ menu.style.left = "auto";
767
+ var menuRect = menu.getBoundingClientRect();
768
+ if (menuRect.bottom > window.innerHeight - 8) menu.style.top = (rect.top - menuRect.height - 2) + "px";
769
+ });
770
+ }
771
+
708
772
  function showLoopCtxMenu(anchorBtn, loopId, loopName, childCount) {
709
773
  closeSessionCtxMenu();
710
774
 
@@ -796,6 +860,40 @@ function startInlineRename(sessionId, currentTitle) {
796
860
  input.addEventListener("click", function (e) { e.stopPropagation(); });
797
861
  }
798
862
 
863
+ function startSplitGroupInlineRename(group) {
864
+ var el = getSessionListEl().querySelector('.split-group-item[data-split-group-id="' + group.id + '"]');
865
+ if (!el) return;
866
+ var textSpan = el.querySelector(".split-group-name");
867
+ if (!textSpan) return;
868
+ var input = document.createElement("input");
869
+ input.type = "text";
870
+ input.className = "session-rename-input";
871
+ input.value = group.name || "Split";
872
+ var original = textSpan.textContent;
873
+ textSpan.innerHTML = "";
874
+ textSpan.appendChild(input);
875
+ input.focus();
876
+ input.select();
877
+ var finished = false;
878
+ function finish(commit) {
879
+ if (finished) return;
880
+ finished = true;
881
+ var name = input.value.trim();
882
+ if (commit && name && name !== group.name && getWs() && store.get('connected')) {
883
+ getWs().send(JSON.stringify({ type: "split_group_rename", id: group.id, name: name }));
884
+ textSpan.textContent = name;
885
+ } else {
886
+ textSpan.textContent = original;
887
+ }
888
+ }
889
+ input.addEventListener("keydown", function (e) {
890
+ if (e.key === "Enter") { e.preventDefault(); finish(true); }
891
+ if (e.key === "Escape") { e.preventDefault(); finish(false); }
892
+ });
893
+ input.addEventListener("blur", function () { finish(true); });
894
+ input.addEventListener("click", function (e) { e.stopPropagation(); });
895
+ }
896
+
799
897
  function startLoopInlineRename(loopId, currentName) {
800
898
  var el = getSessionListEl().querySelector('.session-loop-group[data-loop-id="' + loopId + '"]');
801
899
  if (!el) return;
@@ -1207,6 +1305,48 @@ function renderSessionItem(s) {
1207
1305
  return el;
1208
1306
  }
1209
1307
 
1308
+ function renderSplitGroupItem(group, members) {
1309
+ var el = document.createElement("div");
1310
+ var split = store.get('splitPanes');
1311
+ el.className = "session-item split-group-item" + (split && split.groupId === group.id ? " active" : "");
1312
+ el.dataset.splitGroupId = group.id;
1313
+
1314
+ var text = document.createElement("span");
1315
+ text.className = "session-item-text split-group-text";
1316
+ var html = '<span class="split-group-glyph" aria-hidden="true">' + iconHtml("columns-2") + '</span><span class="split-group-vendors">';
1317
+ for (var i = 0; i < members.length; i++) {
1318
+ var vendor = members[i].vendor || "claude";
1319
+ html += '<img class="session-vendor-icon split-group-vendor" src="' +
1320
+ (VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude) + '" alt="" title="' +
1321
+ escapeHtml(VENDOR_NAMES[vendor] || vendor) + '">';
1322
+ }
1323
+ html += "</span>";
1324
+ if (members[0].isProcessing || members[1].isProcessing) html += '<span class="session-processing"></span>';
1325
+ html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
1326
+ text.innerHTML = html;
1327
+ el.appendChild(text);
1328
+
1329
+ var unread = (members[0].unread || 0) + (members[1].unread || 0);
1330
+ var badge = document.createElement("span");
1331
+ badge.className = "session-unread-badge" + (unread > 0 ? " has-unread" : "");
1332
+ badge.dataset.splitGroupId = group.id;
1333
+ if (unread > 0) badge.textContent = unread > 99 ? "99+" : String(unread);
1334
+ el.appendChild(badge);
1335
+
1336
+ el.addEventListener("click", function () {
1337
+ if (openGroup(group)) {
1338
+ dismissOverlayPanels();
1339
+ closeSidebar();
1340
+ }
1341
+ });
1342
+ el.addEventListener("contextmenu", function (e) {
1343
+ e.preventDefault();
1344
+ e.stopPropagation();
1345
+ showSplitGroupCtxMenu(el, group);
1346
+ });
1347
+ return el;
1348
+ }
1349
+
1210
1350
  // --- Main session list ---
1211
1351
 
1212
1352
  export function renderSessionList(sessions) {
@@ -1219,10 +1359,15 @@ export function renderSessionList(sessions) {
1219
1359
 
1220
1360
  // Partition: loop sessions vs normal sessions
1221
1361
  // Group by loopId + date so all runs of the same task on the same day are merged
1362
+ var splitGroups = store.get('splitGroups') || [];
1363
+ var groupedIds = groupedSessionIds(splitGroups);
1364
+ var sessionsById = new Map();
1365
+ for (var si = 0; si < cachedSessions.length; si++) sessionsById.set(cachedSessions[si].id, cachedSessions[si]);
1222
1366
  var loopGroups = {}; // groupKey -> [sessions]
1223
1367
  var normalSessions = [];
1224
1368
  for (var i = 0; i < cachedSessions.length; i++) {
1225
1369
  var s = cachedSessions[i];
1370
+ if (groupedIds.has(s.id)) continue;
1226
1371
  if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
1227
1372
  // Task crafting sessions live in the scheduler calendar, not the main list (except debate)
1228
1373
  continue;
@@ -1242,6 +1387,18 @@ export function renderSessionList(sessions) {
1242
1387
  for (var j = 0; j < normalSessions.length; j++) {
1243
1388
  items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1244
1389
  }
1390
+ for (var sg = 0; sg < splitGroups.length; sg++) {
1391
+ var splitGroup = splitGroups[sg];
1392
+ var leftMember = sessionsById.get(splitGroup.members[0]);
1393
+ var rightMember = sessionsById.get(splitGroup.members[1]);
1394
+ if (!leftMember || !rightMember) continue;
1395
+ items.push({
1396
+ type: "split-group",
1397
+ group: splitGroup,
1398
+ members: [leftMember, rightMember],
1399
+ lastActivity: Math.max(leftMember.lastActivity || 0, rightMember.lastActivity || 0),
1400
+ });
1401
+ }
1245
1402
  var groupKeys = Object.keys(loopGroups);
1246
1403
  for (var k = 0; k < groupKeys.length; k++) {
1247
1404
  var gk = groupKeys[k];
@@ -1265,6 +1422,8 @@ export function renderSessionList(sessions) {
1265
1422
  if (item.type === "session" && item.data && !isSessionVisibleBySearch(item.data.id)) {
1266
1423
  continue;
1267
1424
  }
1425
+ if (item.type === "split-group" && searchMatchIds !== null &&
1426
+ !searchMatchIds.has(item.members[0].id) && !searchMatchIds.has(item.members[1].id)) continue;
1268
1427
  if (item.type === "session" && item.data && item.data.bookmarked) {
1269
1428
  bookmarkedItems.push(item);
1270
1429
  } else {
@@ -1320,6 +1479,8 @@ export function renderSessionList(sessions) {
1320
1479
  if (loopEl) {
1321
1480
  regularContainer.appendChild(loopEl);
1322
1481
  }
1482
+ } else if (item.type === "split-group") {
1483
+ regularContainer.appendChild(renderSplitGroupItem(item.group, item.members));
1323
1484
  } else {
1324
1485
  regularContainer.appendChild(renderSessionItem(item.data));
1325
1486
  }
@@ -1400,7 +1561,20 @@ function renderPresenceAvatars(el, sessionId) {
1400
1561
  // --- Session badge ---
1401
1562
 
1402
1563
  export function updateSessionBadge(sessionId, count) {
1564
+ for (var i = 0; i < cachedSessions.length; i++) {
1565
+ if (cachedSessions[i].id === sessionId) cachedSessions[i].unread = count;
1566
+ }
1403
1567
  var badge = document.querySelector('.session-unread-badge[data-session-id="' + sessionId + '"]');
1568
+ var groups = store.get('splitGroups') || [];
1569
+ for (var gi = 0; gi < groups.length; gi++) {
1570
+ if (groups[gi].members.indexOf(sessionId) === -1) continue;
1571
+ badge = document.querySelector('.session-unread-badge[data-split-group-id="' + groups[gi].id + '"]');
1572
+ count = 0;
1573
+ for (var mi = 0; mi < cachedSessions.length; mi++) {
1574
+ if (groups[gi].members.indexOf(cachedSessions[mi].id) !== -1) count += cachedSessions[mi].unread || 0;
1575
+ }
1576
+ break;
1577
+ }
1404
1578
  if (!badge) return;
1405
1579
  if (count > 0) {
1406
1580
  badge.textContent = count > 99 ? "99+" : String(count);
@@ -12,6 +12,7 @@ import {
12
12
  getCurrentDmUserId
13
13
  } from './sidebar-mates.js';
14
14
  import { initSidebarMobile } from './sidebar-mobile.js';
15
+ import { store } from './store.js';
15
16
 
16
17
  var ctx;
17
18
  var _syncResizeHandles = null;
@@ -26,6 +27,7 @@ export function dismissOverlayPanels() {
26
27
  }
27
28
 
28
29
  export function updatePageTitle() {
30
+ if (store.get('paneMode')) return;
29
31
  var sessionTitle = "";
30
32
  var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
31
33
  if (activeItem) sessionTitle = activeItem.textContent;