clay-server 5.5.0-beta.1 → 5.5.0-beta.3

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 (123) hide show
  1. package/lib/clay-product-control-input.js +2 -2
  2. package/lib/clay-product-control-tools.js +1 -1
  3. package/lib/clay-product-control.js +4 -3
  4. package/lib/daemon-project-access.js +10 -2
  5. package/lib/daemon-projects.js +16 -4
  6. package/lib/daemon-worktree-controls.js +171 -0
  7. package/lib/daemon.js +14 -63
  8. package/lib/personal-provider-install.js +85 -0
  9. package/lib/personal-provider-settings.js +94 -0
  10. package/lib/project-logs-service.js +2 -2
  11. package/lib/project-pair-task-control.js +1 -1
  12. package/lib/project-pending-message-queue.js +6 -2
  13. package/lib/project-product-control.js +1 -0
  14. package/lib/project-session-actions.js +46 -3
  15. package/lib/project-session-pair.js +2 -0
  16. package/lib/project-sessions.js +11 -3
  17. package/lib/project-user-message.js +5 -1
  18. package/lib/project-vendor-login.js +95 -32
  19. package/lib/project-worker-auth.js +71 -0
  20. package/lib/project-worker-conversation.js +11 -2
  21. package/lib/project.js +9 -0
  22. package/lib/public/app.js +22 -0
  23. package/lib/public/css/icon-strip.css +5 -0
  24. package/lib/public/css/overlays.css +18 -0
  25. package/lib/public/css/settings-shell.css +14 -0
  26. package/lib/public/css/settings-ui.css +6 -0
  27. package/lib/public/css/split-view-layout.css +16 -11
  28. package/lib/public/modules/agent-message-bubbles.js +33 -4
  29. package/lib/public/modules/app-messages.js +7 -1
  30. package/lib/public/modules/app-projects.js +3 -4
  31. package/lib/public/modules/clay-control-copy.js +1 -0
  32. package/lib/public/modules/clay-tool-presentation.js +1 -1
  33. package/lib/public/modules/generation-stop.js +4 -4
  34. package/lib/public/modules/pane-activation.js +2 -2
  35. package/lib/public/modules/pane-bridge.js +4 -2
  36. package/lib/public/modules/personal-provider-context.js +11 -0
  37. package/lib/public/modules/project-settings.js +1 -1
  38. package/lib/public/modules/settings-section-nav.js +58 -0
  39. package/lib/public/modules/settings-shell-registry.js +4 -2
  40. package/lib/public/modules/settings-shell.js +17 -6
  41. package/lib/public/modules/sidebar-projects.js +23 -9
  42. package/lib/public/modules/sidebar-sessions.js +3 -0
  43. package/lib/public/modules/split-pane-force-stop.js +47 -0
  44. package/lib/public/modules/split-pane-header-state.js +11 -1
  45. package/lib/public/modules/split-pane-renderer.js +2 -2
  46. package/lib/public/modules/split-view-arrange-menu.js +38 -0
  47. package/lib/public/modules/split-view-create-menu.js +9 -5
  48. package/lib/public/modules/split-view-member-actions.js +42 -0
  49. package/lib/public/modules/split-view-picker-actions.js +2 -2
  50. package/lib/public/modules/split-view-presentation-renderer.js +10 -35
  51. package/lib/public/modules/split-view-presentations.js +9 -8
  52. package/lib/public/modules/split-view-sidebar-menu.js +2 -1
  53. package/lib/public/modules/split-view-workers.js +32 -17
  54. package/lib/public/modules/stt-settings.js +21 -4
  55. package/lib/public/modules/user-settings-layout.js +11 -8
  56. package/lib/public/modules/user-settings-provider-install.js +42 -0
  57. package/lib/public/modules/user-settings-providers.js +102 -0
  58. package/lib/public/modules/user-settings-voice.js +50 -41
  59. package/lib/public/modules/vendor-login-browser.js +1 -1
  60. package/lib/public/modules/vendor-login.js +72 -12
  61. package/lib/public/modules/view-workspace-state.js +1 -1
  62. package/lib/public/modules/worker-activity-indicator.js +49 -0
  63. package/lib/public/modules/worker-message-previews.js +47 -11
  64. package/lib/public/modules/worker-stop-controls.js +36 -0
  65. package/lib/public/modules/worktree-controls.js +86 -0
  66. package/lib/public/pane-assets/{app-4IZ6F6ND.js → app-ANJS2LSK.js} +1 -1
  67. package/lib/public/pane-assets/{chunk-L3FCTD7U.js → chunk-32YIYF7R.js} +1 -1
  68. package/lib/public/pane-assets/{chunk-RRYWV5JT.js → chunk-3EJ5IAHZ.js} +1 -1
  69. package/lib/public/pane-assets/{chunk-3E6LDNXF.js → chunk-3TIVBAKM.js} +1 -1
  70. package/lib/public/pane-assets/{chunk-I6X5J2V5.js → chunk-4QFNLHW5.js} +1 -1
  71. package/lib/public/pane-assets/{chunk-OB3KKICE.js → chunk-5XMZMIFG.js} +1 -1
  72. package/lib/public/pane-assets/chunk-6S2C7Q2N.js +1 -0
  73. package/lib/public/pane-assets/chunk-7B2GJI57.js +1 -0
  74. package/lib/public/pane-assets/{chunk-A7OB7HQS.js → chunk-BOFNCPTN.js} +1 -1
  75. package/lib/public/pane-assets/{chunk-ELL4UBGY.js → chunk-CXWAJEN5.js} +1 -1
  76. package/lib/public/pane-assets/{chunk-SEELA2D2.js → chunk-EWTXBZJO.js} +1 -1
  77. package/lib/public/pane-assets/{chunk-ADGKPVFD.js → chunk-G35ZOJGP.js} +1 -1
  78. package/lib/public/pane-assets/chunk-HYT24UBT.js +1 -0
  79. package/lib/public/pane-assets/chunk-ISQEPZMY.js +1 -0
  80. package/lib/public/pane-assets/{chunk-6ZF2FJT3.js → chunk-JM46WGDH.js} +1 -1
  81. package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
  82. package/lib/public/pane-assets/{chunk-YXJ7ISEK.js → chunk-P3AK4ORJ.js} +1 -1
  83. package/lib/public/pane-assets/chunk-QKPGE7DN.js +1 -0
  84. package/lib/public/pane-assets/chunk-TS5JUEVH.js +1 -0
  85. package/lib/public/pane-assets/{chunk-YWHBNEB2.js → chunk-UGH5MCNK.js} +1 -1
  86. package/lib/public/pane-assets/{chunk-YFUORWA6.js → chunk-V5DLPSLT.js} +1 -1
  87. package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
  88. package/lib/public/pane-assets/{chunk-A4MLSYBJ.js → chunk-XSSTR6GC.js} +1 -1
  89. package/lib/public/pane-assets/{chunk-DJ5WXQKT.js → chunk-XVESKMU3.js} +1 -1
  90. package/lib/public/pane-assets/manifest.json +443 -433
  91. package/lib/public/pane-assets/style-VBVCWV44.css +1 -0
  92. package/lib/sdk-bridge.js +40 -6
  93. package/lib/sdk-force-stop.js +62 -0
  94. package/lib/sdk-message-processor.js +2 -1
  95. package/lib/server-worktree-controls.js +34 -0
  96. package/lib/server.js +11 -1
  97. package/lib/session-force-stop-support.js +15 -0
  98. package/lib/sessions.js +6 -1
  99. package/lib/terminal-manager.js +1 -1
  100. package/lib/terminal.js +13 -1
  101. package/lib/vendor-login-browser-flow.js +1 -1
  102. package/lib/worker-activity.js +26 -0
  103. package/lib/worker-message-previews.js +12 -7
  104. package/lib/worktree-branch.js +57 -0
  105. package/lib/worktree-identity.js +39 -0
  106. package/lib/worktree-preferences.js +25 -0
  107. package/lib/worktree-ref-transaction.js +36 -0
  108. package/lib/yoke/adapters/antigravity.js +3 -0
  109. package/lib/yoke/force-termination.js +26 -0
  110. package/lib/yoke/interface.js +14 -0
  111. package/lib/yoke/process-force-termination.js +24 -0
  112. package/lib/yoke/provider-install-recipes.js +20 -0
  113. package/lib/yoke/vendor-registry.js +20 -1
  114. package/package.json +1 -1
  115. package/lib/public/pane-assets/chunk-6KO36HMU.js +0 -1
  116. package/lib/public/pane-assets/chunk-FILRACQB.js +0 -168
  117. package/lib/public/pane-assets/chunk-H6MU72QE.js +0 -1
  118. package/lib/public/pane-assets/chunk-PJURCIJQ.js +0 -1
  119. package/lib/public/pane-assets/chunk-Q7RFSS5Q.js +0 -1
  120. package/lib/public/pane-assets/chunk-UTLQWAKG.js +0 -1
  121. package/lib/public/pane-assets/chunk-WE4NWXNK.js +0 -1
  122. package/lib/public/pane-assets/chunk-Z2MHS43B.js +0 -1
  123. package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
@@ -1,3 +1,4 @@
1
+ import { activateSettingsSections, deactivateSettingsSections } from './settings-section-nav.js';
1
2
  // Unified full-screen settings: one shell for Personal, Project and Server
2
3
  // settings. Scope modules register a controller; the shell owns routing,
3
4
  // access, project target binding, focus, close restoration and draft guards.
@@ -134,6 +135,7 @@ function applyRoute(route, focus) {
134
135
  var resolved = resolveRoute(route, shell.access);
135
136
  var ctrl = controllers[resolved.scope];
136
137
  if (!ctrl) return;
138
+ deactivateSettingsSections();
137
139
  if (shell.scope && shell.scope !== resolved.scope && controllers[shell.scope].leave) controllers[shell.scope].leave();
138
140
  var entering = shell.scope !== resolved.scope;
139
141
  shell.scope = resolved.scope;
@@ -152,7 +154,8 @@ function applyRoute(route, focus) {
152
154
  markActivePage(page);
153
155
  store.set({ settingsLastRoute: { scope: resolved.scope, page: page, slug: shell.slug } });
154
156
  enhanceSettingsSelects(root); refreshIcons(root);
155
- if (focus) focusHeading(ctrl);
157
+ if (focus && !resolved.anchor) focusHeading(ctrl);
158
+ activateSettingsSections(page, resolved.anchor);
156
159
  }
157
160
 
158
161
  function onScopeSelect(scope) {
@@ -160,12 +163,18 @@ function onScopeSelect(scope) {
160
163
  guard('scope', function () { applyRoute({ scope: scope, page: shell.pages[scope], slug: scope === 'project' ? currentRoute().slug : null }, true); });
161
164
  }
162
165
  function onPageSelect(page) {
163
- if (page === shell.pages[shell.scope]) { focusHeading(controller()); return; }
166
+ var route = resolveRoute({scope:shell.scope,page:page}, shell.access);
167
+ if (route.page === shell.pages[shell.scope]) {
168
+ if (!route.anchor) focusHeading(controller());
169
+ activateSettingsSections(route.page, route.anchor); return;
170
+ }
164
171
  guard('page', function () {
165
172
  var ctrl = controller();
166
- shell.pages[shell.scope] = page; ctrl.activate(page, { access: shell.access, scope: shell.scope });
167
- markActivePage(page); store.set({ settingsLastRoute: { scope: shell.scope, page: page, slug: shell.slug } });
168
- focusHeading(ctrl);
173
+ deactivateSettingsSections();
174
+ shell.pages[shell.scope] = route.page; ctrl.activate(route.page, {access:shell.access,scope:shell.scope});
175
+ markActivePage(route.page); store.set({settingsLastRoute:{scope:shell.scope,page:route.page,slug:shell.slug}});
176
+ if (!route.anchor) focusHeading(ctrl);
177
+ activateSettingsSections(route.page, route.anchor);
169
178
  }, shell.pages[shell.scope]);
170
179
  }
171
180
  // A project switch retargets only the settings view, never the workspace.
@@ -181,7 +190,8 @@ export function navigateSettings(route) {
181
190
  var resolved = resolveRoute(route, shell.access);
182
191
  if (resolved.scope === shell.scope && resolved.page === shell.pages[shell.scope] && (resolved.scope !== 'project' || !route.slug || route.slug === shell.slug)) {
183
192
  setShellStatus(redirectNote(resolved), resolved.redirected ? 'error' : undefined);
184
- focusHeading(controller()); return;
193
+ if (!resolved.anchor) focusHeading(controller());
194
+ activateSettingsSections(resolved.page, resolved.anchor); return;
185
195
  }
186
196
  guard(resolved.scope === shell.scope ? (route.slug && route.slug !== shell.slug ? 'project' : 'page') : 'scope', function () { applyRoute(route, true); }, shell.pages[shell.scope]);
187
197
  }
@@ -251,6 +261,7 @@ export function closeSettings(opts) {
251
261
  function finish() {
252
262
  shell.token += 1; binding.invalidate();
253
263
  var ctrl = controller();
264
+ deactivateSettingsSections();
254
265
  if (ctrl && ctrl.leave) ctrl.leave();
255
266
  Object.keys(controllers).forEach(function (id) { if (controllers[id].close) controllers[id].close(); });
256
267
  root.classList.add('hidden');
@@ -1,3 +1,4 @@
1
+ import { reviewWorktreeRemoval } from './worktree-controls.js';
1
2
  // sidebar-projects.js - Project icon strip, context menus, emoji picker, drag-and-drop, worktree modal
2
3
  // Extracted from sidebar.js (PR-36)
3
4
 
@@ -433,8 +434,9 @@ export function showProjectCtxMenu(anchorEl, slug, name, icon, position, options
433
434
  }
434
435
 
435
436
  var isWorktree = slug.indexOf("--") !== -1;
436
- var canRemoveTarget = !isWorktree || options.canRemoveWorktree !== false;
437
- var canDeleteTarget = canRemoveTarget && (!store.get('permissions') || store.get('permissions').deleteProject !== false);
437
+ var ownsWorktree = !isWorktree || !store.get('isMultiUserMode') || !!(targetProject && targetProject.projectOwnerId === store.get('myUserId'));
438
+ if (!ownsWorktree) iconItem.disabled = true;
439
+ var canDeleteTarget = ownsWorktree && (!store.get('permissions') || store.get('permissions').deleteProject !== false);
438
440
  if (!isWorktree || canDeleteTarget) {
439
441
  var sep2 = document.createElement("div");
440
442
  sep2.className = "project-ctx-separator";
@@ -465,15 +467,26 @@ export function showProjectCtxMenu(anchorEl, slug, name, icon, position, options
465
467
  var deleteItem = document.createElement("button");
466
468
  deleteItem.className = "project-ctx-item project-ctx-delete";
467
469
  deleteItem.setAttribute("role", "menuitem");
468
- deleteItem.innerHTML = iconHtml("trash-2") + " <span>" + (isWorktree ? "Remove Worktree" : "Remove Project") + "</span>";
470
+ deleteItem.innerHTML = iconHtml("trash-2") + " <span>" + (isWorktree ? "Exclude worktree (keep files)" : "Remove Project") + "</span>";
469
471
  deleteItem.addEventListener("click", function (e) {
470
472
  e.stopPropagation();
471
473
  closeProjectCtxMenu();
472
- if (getWs() && store.get('connected')) {
474
+ if (isWorktree) reviewWorktreeRemoval(slug, "exclude", anchorEl);
475
+ else if (getWs() && store.get('connected')) {
473
476
  getWs().send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name }));
474
477
  }
475
478
  });
476
479
  menu.appendChild(deleteItem);
480
+ if (isWorktree) {
481
+ var diskItem = document.createElement("button");
482
+ diskItem.className = "project-ctx-item project-ctx-delete";
483
+ diskItem.setAttribute("role", "menuitem");
484
+ diskItem.innerHTML = iconHtml("trash-2") + " <span>Delete worktree…</span>";
485
+ diskItem.addEventListener("click", function (event) {
486
+ event.stopPropagation(); closeProjectCtxMenu(); reviewWorktreeRemoval(slug, "delete", anchorEl);
487
+ });
488
+ menu.appendChild(diskItem);
489
+ }
477
490
  }
478
491
 
479
492
  document.body.appendChild(menu);
@@ -1103,7 +1116,7 @@ export function renderIconStrip(projects, currentSlug) {
1103
1116
  } else {
1104
1117
  var abbrev = document.createElement("span");
1105
1118
  abbrev.className = "wt-branch-abbrev";
1106
- abbrev.textContent = getProjectAbbrev(wt.name);
1119
+ abbrev.innerHTML = iconHtml("git-branch");
1107
1120
  wtEl.appendChild(abbrev);
1108
1121
  }
1109
1122
 
@@ -1124,13 +1137,13 @@ export function renderIconStrip(projects, currentSlug) {
1124
1137
  });
1125
1138
  })(wt.slug);
1126
1139
 
1127
- (function (slug, name, elem, external) {
1140
+ (function (slug, name, elem) {
1128
1141
  elem.addEventListener("contextmenu", function (e) {
1129
1142
  e.preventDefault();
1130
1143
  e.stopPropagation();
1131
- showProjectCtxMenu(elem, slug, name, null, null, { shareTargetSlug: true, canRemoveWorktree: !external });
1144
+ showProjectCtxMenu(elem, slug, name, null, null, { shareTargetSlug: true });
1132
1145
  });
1133
- })(wt.slug, wt.name, wtEl, isExternal);
1146
+ })(wt.slug, wt.name, wtEl);
1134
1147
 
1135
1148
  if (!isWtActive && (wt.pendingPermissions > 0 || getPendingTuiAttention(wt.slug) > 0)) {
1136
1149
  wtEl.classList.add("has-pending-perm");
@@ -1250,7 +1263,8 @@ function createMobileProjectItem(p, currentSlug, isWorktree) {
1250
1263
  abbrev.textContent = p.icon;
1251
1264
  parseEmojis(abbrev);
1252
1265
  } else {
1253
- abbrev.textContent = getProjectAbbrev(p.name);
1266
+ if (p.isWorktree) abbrev.innerHTML = iconHtml("git-branch");
1267
+ else abbrev.textContent = getProjectAbbrev(p.name);
1254
1268
  }
1255
1269
  el.appendChild(abbrev);
1256
1270
 
@@ -1,3 +1,4 @@
1
+ import { appendRemoveFromView } from './split-view-member-actions.js';
1
2
  import { appendGithubWork } from './session-github.js';
2
3
  // sidebar-sessions.js - Session list, search, presence, countdown, CLI picker
3
4
  // Extracted from sidebar.js (PR-35)
@@ -290,6 +291,8 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData, po
290
291
  menu.appendChild(renameItem);
291
292
 
292
293
 
294
+ appendRemoveFromView(menu, sessionData, currentAnchor);
295
+
293
296
  // Session visibility toggle (only the session owner can change)
294
297
  if (!isMateWorkspace(store.snap()) && store.get('isMultiUserMode') && sessionData && !sessionData.isMate && sessionData.ownerId && sessionData.ownerId === store.get('myUserId')) {
295
298
  var currentVis = (sessionData && sessionData.sessionVisibility) || "private";
@@ -0,0 +1,47 @@
1
+ import { iconHtml } from './icons.js';
2
+ import { livePaneBinding, samePaneBinding } from './pane-binding.js';
3
+ import { syncPaneHeaderState } from './split-pane-header-state.js';
4
+ import { showConfirm, hideConfirm } from './app-misc.js';
5
+
6
+ export function createPaneForceStop(paneEl, frame) {
7
+ var button = document.createElement('button');
8
+ button.type = 'button'; button.className = 'split-pane-force-stop';
9
+ button.setAttribute('aria-label', 'Force stop session'); button.title = 'Force stop session and all its Workers';
10
+ button.innerHTML = iconHtml('power');
11
+ button.addEventListener('click', function (event) {
12
+ event.stopPropagation();
13
+ if (!syncPaneHeaderState(paneEl) || button.disabled || !frame.contentWindow || !frame.clayPane || !livePaneBinding(frame)) return;
14
+ var binding = frame.clayPane.identity().binding, sourceDocument = frame.contentDocument, settled = false;
15
+ var title = paneEl.querySelector('.split-pane-title');
16
+ var text = 'Force stop all work in ' + (title && title.textContent || 'this session') + ' and all of its Workers? Queued messages will be paused. The pane and conversation history will stay open. Commands or external requests already sent may still be running.';
17
+ function current() {
18
+ return paneEl.isConnected && !paneEl.hidden && !paneEl.inert && frame.contentDocument === sourceDocument &&
19
+ syncPaneHeaderState(paneEl) && !button.disabled && livePaneBinding(frame, { paneBinding: binding }) && samePaneBinding(binding, frame.clayPane.identity().binding);
20
+ }
21
+ var modal = document.getElementById('confirm-modal'), panel = modal && modal.querySelector('.confirm-dialog');
22
+ var cancel = document.getElementById('confirm-cancel'), ok = document.getElementById('confirm-ok');
23
+ function cleanup() {
24
+ settled = true;
25
+ window.removeEventListener('keydown', onKey, true);
26
+ if (panel) { panel.removeAttribute('role'); panel.removeAttribute('aria-modal'); panel.removeAttribute('aria-labelledby'); }
27
+ if (current()) button.focus();
28
+ }
29
+ function onKey(event) {
30
+ if (event.key === 'Escape') { event.preventDefault(); event.stopImmediatePropagation(); hideConfirm(); return; }
31
+ if (event.key !== 'Tab') return;
32
+ event.preventDefault(); event.stopImmediatePropagation();
33
+ var controls = [cancel, ok].filter(Boolean), index = controls.indexOf(document.activeElement);
34
+ controls[(index + (event.shiftKey ? controls.length - 1 : 1)) % controls.length].focus();
35
+ }
36
+ showConfirm(text, function () {
37
+ if (settled) return;
38
+ var valid = current(); cleanup();
39
+ if (!valid) return;
40
+ frame.contentWindow.postMessage({ type: 'clay-pane-force-stop', paneBinding: binding }, window.location.origin);
41
+ }, 'Force stop all work', true, cleanup);
42
+ if (panel) { panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); panel.setAttribute('aria-labelledby', 'confirm-text'); }
43
+ window.addEventListener('keydown', onKey, true);
44
+ if (cancel) cancel.focus();
45
+ });
46
+ return button;
47
+ }
@@ -20,7 +20,7 @@ export function syncPaneHeaderState(paneEl) {
20
20
  header.setAttribute('aria-busy', ready ? 'false' : 'true');
21
21
  header.dataset.paneLoading = ready ? 'false' : 'true';
22
22
  header.inert = !ready;
23
- var actions = header.querySelectorAll('.split-pane-info, .split-pane-search, .split-pane-context, .split-pane-sidebar, .split-view-arrange-trigger');
23
+ var actions = header.querySelectorAll('.split-pane-info, .split-pane-force-stop, .split-pane-search, .split-pane-context, .split-pane-sidebar, .split-view-arrange-trigger');
24
24
  for (var i = 0; i < actions.length; i++) {
25
25
  var action = actions[i];
26
26
  if (!ready) {
@@ -31,6 +31,16 @@ export function syncPaneHeaderState(paneEl) {
31
31
  delete action.dataset.panePriorDisabled;
32
32
  }
33
33
  }
34
+ var force = header.querySelector('.split-pane-force-stop');
35
+ if (force) {
36
+ var session = getCachedSessions().find(function (item) { return item.id === Number(paneEl.dataset.sessionId); });
37
+ var support = session && session.forceTermination;
38
+ var reason = support && support.reason || 'Force termination is not yet supported for this vendor';
39
+ force.disabled = !ready || !support || support.supported !== true;
40
+ force.title = support && support.supported === true ? 'Force stop session and all its Workers' : reason;
41
+ force.setAttribute('aria-label', 'Force stop session' + (support && support.supported === true ? '' : ': ' + reason));
42
+ force.setAttribute('aria-disabled', force.disabled ? 'true' : 'false');
43
+ }
34
44
  var close = paneEl.querySelector('.split-pane-close');
35
45
  if (close) {
36
46
  var host = paneEl.parentNode;
@@ -1,3 +1,4 @@
1
+ import { createPaneForceStop } from './split-pane-force-stop.js';
1
2
  import { renderPaneRuntime } from './split-pane-runtime.js';
2
3
  import { store } from './store.js';
3
4
  import { getWs } from './ws-ref.js';
@@ -201,7 +202,6 @@ function createPaneSearchPopover(paneEl, frame, searchButton) {
201
202
  throw error;
202
203
  }
203
204
  }
204
-
205
205
  export function handlePaneSearchMessage(event, host) {
206
206
  var frames = host ? host.querySelectorAll(".split-pane-frame") : [];
207
207
  for (var i = 0; i < frames.length; i++) {
@@ -287,7 +287,6 @@ function isConfiguredWorker(groups, sessionId) {
287
287
  export function updatePaneFullAccessButton(button, session) {
288
288
  if (button) renderPermissionControl(button, permissionState(session));
289
289
  }
290
-
291
290
  export function createSplitPane(pane, closePane) {
292
291
  var paneEl = document.createElement("section"); paneEl.className = "split-pane";
293
292
  var header = document.createElement("header"); header.className = "split-pane-header"; header.title = pane.title;
@@ -345,6 +344,7 @@ export function createSplitPane(pane, closePane) {
345
344
  }); header.appendChild(sidebarButton);
346
345
  var search = document.createElement("button"); search.type = "button"; search.className = "split-pane-search"; search.setAttribute("aria-label", "Search this session"); search.title = "Search this session"; search.innerHTML = iconHtml("search");
347
346
  header.insertBefore(search, close);
347
+ header.insertBefore(createPaneForceStop(paneEl, frame), close);
348
348
  function refreshReadyHeader() {
349
349
  if (paneEl.classList.contains('split-pane-cached')) return;
350
350
  var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId), current = sessionById(id);
@@ -0,0 +1,38 @@
1
+ import { iconHtml } from './icons.js';
2
+ import { appendLayoutOption } from './split-view-layout-icons.js';
3
+ import { SPLIT_SIDES } from './split-view-picker-render.js';
4
+
5
+ export function ensureArrangeMenu(closePresentationArrange) {
6
+ var menu = document.getElementById("split-view-arrange-menu");
7
+ if (menu) return menu;
8
+ menu = document.createElement("div"); menu.id = "split-view-arrange-menu"; menu.className = "split-view-arrange";
9
+ menu.setAttribute("role", "dialog"); menu.setAttribute("aria-label", "Arrange Split view"); menu.hidden = true;
10
+ var header = document.createElement("div"); header.className = "split-view-arrange-header";
11
+ var title = document.createElement("strong"); title.textContent = "Arrange Split view"; header.appendChild(title);
12
+ var close = document.createElement("button"); close.type = "button"; close.setAttribute("aria-label", "Close arrangement menu"); close.innerHTML = iconHtml("x"); header.appendChild(close);
13
+ close.addEventListener("click", function () { closePresentationArrange(); }); menu.appendChild(header);
14
+ var presets = [["balanced", "Balanced"], ["columns", "Columns"], ["rows", "Rows"], ["grid", "Grid"], ["main-left", "Main left"], ["main-right", "Main right"]];
15
+ var presetGrid = document.createElement("div"); presetGrid.className = "split-view-arrange-grid";
16
+ presets.forEach(function (item) { var button = document.createElement("button"); button.type = "button"; button.dataset.preset = item[0]; appendLayoutOption(button, item[1], "preset", item[0]); presetGrid.appendChild(button); });
17
+ menu.appendChild(presetGrid);
18
+ var positions = [["left", "Left half"], ["right", "Right half"], ["top", "Top half"], ["bottom", "Bottom half"], ["top-left", "Top left"], ["top-right", "Top right"], ["bottom-left", "Bottom left"], ["bottom-right", "Bottom right"], ["left-third", "Left third"], ["right-third", "Right third"]];
19
+ var positionGrid = document.createElement("div"); positionGrid.className = "split-view-arrange-grid split-view-arrange-positions";
20
+ positions.forEach(function (item) { var button = document.createElement("button"); button.type = "button"; button.dataset.position = item[0]; appendLayoutOption(button, item[1], "position", item[0]); positionGrid.appendChild(button); });
21
+ menu.appendChild(positionGrid);
22
+ var additions = document.createElement('div'); additions.className = 'split-session-directions';
23
+ SPLIT_SIDES.forEach(function (side) {
24
+ var button = document.createElement('button'); button.type = 'button'; button.dataset.arrangeAddSide = side[0];
25
+ button.innerHTML = iconHtml(side[2]); var label = document.createElement('span'); label.textContent = 'Add ' + side[1] + ' Split'; button.appendChild(label); additions.appendChild(button);
26
+ });
27
+ menu.appendChild(additions);
28
+ var focusButton = document.createElement("button"); focusButton.type = "button"; focusButton.dataset.focusLayout = "true"; menu.appendChild(focusButton);
29
+ var membership = document.createElement("button"); membership.type = "button"; membership.dataset.arrangeMembership = "true";
30
+ membership.textContent = "Choose sessions"; menu.appendChild(membership);
31
+ var context = document.createElement("div"); context.className = "split-view-arrange-context";
32
+ var contextButton = document.createElement("button"); contextButton.type = "button"; contextButton.dataset.arrangeContext = "true";
33
+ contextButton.setAttribute("aria-expanded", "false"); contextButton.setAttribute("aria-controls", "split-view-arrange-context-list");
34
+ var contextTitle = document.createElement("span"); contextTitle.dataset.arrangeContextTitle = "true"; contextButton.appendChild(contextTitle); context.appendChild(contextButton);
35
+ var contextList = document.createElement("div"); contextList.id = "split-view-arrange-context-list"; contextList.hidden = true; contextList.setAttribute("role", "listbox");
36
+ context.appendChild(contextList); menu.insertBefore(context, presetGrid);
37
+ document.body.appendChild(menu); return menu;
38
+ }
@@ -29,8 +29,8 @@ function position() {
29
29
  menu.style.left = Math.max(8, Math.min(rect.right - width, innerWidth - width - 8)) + 'px';
30
30
  menu.style.top = Math.max(8, Math.min(rect.bottom + 5, innerHeight - menu.offsetHeight - 8)) + 'px';
31
31
  }
32
- function open(anchor) {
33
- var receipt = pickerReceipt(anchor);
32
+ function open(anchor, side, origin) {
33
+ var receipt = pickerReceipt(anchor, origin);
34
34
  if (!receipt || !pickerCurrent(receipt)) { showToast('Wait for this View to finish loading, then try again.', 'warn'); return; }
35
35
  var menu = menuElement();
36
36
  if (menu && !menu.hidden && menu.clayCreateAnchor === anchor) { close(true); return; }
@@ -41,8 +41,12 @@ function open(anchor) {
41
41
  }
42
42
  receipt.reservationId = 'split-picker-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
43
43
  menu.clayCreateAnchor = anchor; anchor.setAttribute('aria-expanded', 'true');
44
- store.set({ splitSessionPicker: { receipt: receipt, side: null, query: '', error: '', priorFocus: store.get('projectSplitViewFocusedOrigin') } });
45
- renderPicker(menu); menu.hidden = false; position(); menu.querySelector('[data-split-side]').focus();
44
+ store.set({ splitSessionPicker: { receipt: receipt, side: side || null, query: '', error: '', priorFocus: store.get('projectSplitViewFocusedOrigin') } });
45
+ if (side) store.set({ projectSplitViewFocusedOrigin: null });
46
+ renderPicker(menu); menu.hidden = false;
47
+ if (side) document.dispatchEvent(new CustomEvent('clay-split-picker-layout'));
48
+ position(); menu.querySelector(side ? 'input' : '[data-split-side]').focus();
49
+ if (side) retryOptions();
46
50
  }
47
51
  function onClick(event) {
48
52
  var menu = menuElement(), state = store.get('splitSessionPicker'); if (!menu || menu.hidden || !state) return;
@@ -76,7 +80,7 @@ function onKey(event) {
76
80
  items[next].focus();
77
81
  }
78
82
  export function initSplitViewCreateMenu() {
79
- document.addEventListener('clay-split-view-open-create', function (event) { if (event.detail && event.detail.anchor) open(event.detail.anchor); });
83
+ document.addEventListener('clay-split-view-open-create', function (event) { if (event.detail && event.detail.anchor) open(event.detail.anchor, event.detail.side, event.detail.origin); });
80
84
  document.addEventListener('click', onClick, true); document.addEventListener('keydown', onKey, true);
81
85
  document.addEventListener('input', function (event) {
82
86
  var state = store.get('splitSessionPicker'); if (!state || !event.target.matches('#split-view-create-menu input')) return;
@@ -0,0 +1,42 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { mutation } from './view-mutations.js';
4
+ import { remove, validateTree } from './split-view-layout-client.js';
5
+ import { showToast } from './utils.js';
6
+ import { iconHtml } from './icons.js';
7
+ import { closeSessionContextMenu } from './session-context-menu.js';
8
+
9
+ export function removeViewSession(view, origin) {
10
+ var live = (store.get('projectSplitViews') || []).find(function (item) { return item.id === view.id && item.revision === view.revision; });
11
+ if (!live || !live.sessions.some(function (session) { return session.sessionOriginId === origin; })) return false;
12
+ var active = live.id === store.get('projectSplitViewActiveId');
13
+ if (active) {
14
+ try {
15
+ document.querySelectorAll('#split-host .split-pane-frame').forEach(function (frame) { if (frame.clayPane) frame.clayPane.checkDraft(); });
16
+ } catch (error) { showToast(error.message, 'warn'); return false; }
17
+ }
18
+ var origins = live.sessions.map(function (session) { return session.sessionOriginId; }).filter(function (item) { return item !== origin; });
19
+ var tree = remove(live.tree, origin);
20
+ if (!validateTree(tree, origins).ok) return false;
21
+ var nextActive = live.activeSessionOriginId === origin ? origins[0] || null : live.activeSessionOriginId;
22
+ return mutation('project_split_view_update', { id: live.id, sessionOrigins: origins, tree: tree,
23
+ layout: live.layout, activeSessionOriginId: nextActive, active: active }, active ? { closedOrigin: origin, activeOrigin: nextActive } : null);
24
+ }
25
+
26
+ export function appendRemoveFromView(menu, session, currentAnchor) {
27
+ if (!session || session.sessionRole === 'worker') return;
28
+ var view = (store.get('projectSplitViews') || []).find(function (item) {
29
+ return item.sessions.length > 1 && item.sessions.some(function (ref) { return ref.sessionOriginId === session.sessionOriginId; });
30
+ });
31
+ if (!view) return;
32
+ var socket = getWs(), slug = store.get('currentSlug'), viewer = store.get('myUserId'), generation = store.get('projectSplitViewSocketGeneration');
33
+ var button = document.createElement('button'); button.className = 'session-ctx-item';
34
+ button.innerHTML = iconHtml('panel-left-close') + ' <span>Remove from View</span>';
35
+ button.disabled = !!store.get('projectSplitViewPending');
36
+ button.addEventListener('click', function (event) {
37
+ event.stopPropagation(); var anchor = currentAnchor(); closeSessionContextMenu(false);
38
+ if (!anchor || socket !== getWs() || slug !== store.get('currentSlug') || viewer !== store.get('myUserId') || generation !== store.get('projectSplitViewSocketGeneration')) return;
39
+ removeViewSession(view, session.sessionOriginId);
40
+ });
41
+ menu.appendChild(button);
42
+ }
@@ -11,9 +11,9 @@ import { checkComposerDeparture } from './session-rich-drafts.js';
11
11
  import { createOptions } from './session-create.js';
12
12
  import { showToast } from './utils.js';
13
13
 
14
- export function pickerReceipt(anchor) {
14
+ export function pickerReceipt(anchor, targetOrigin) {
15
15
  var view = activeSavedView(), pane = anchor && anchor.closest('.split-pane'), frame = pane && pane.querySelector('.split-pane-frame');
16
- var origin = pane && pane.dataset.presentationOrigin || frame && frame.dataset.sessionOriginId || store.get('sessionOriginId');
16
+ var origin = targetOrigin || pane && pane.dataset.presentationOrigin || frame && frame.dataset.sessionOriginId || store.get('sessionOriginId');
17
17
  return view && view.sessions.some(function (session) { return session.sessionOriginId === origin; }) ? { scope: layoutScope(view), origin: origin } : null;
18
18
  }
19
19
  export function pickerCurrent(receipt) {
@@ -16,7 +16,8 @@ import { createSplitPane, updateSplitPaneMetadata, updateSplitPaneCloseVisibilit
16
16
  import { syncPaneHeaderState } from './split-pane-header-state.js';
17
17
  import { rectangles, resize, validateTree, listIds } from './split-view-layout-client.js';
18
18
  import { iconHtml, refreshIcons } from './icons.js';
19
- import { appendLayoutOption } from './split-view-layout-icons.js';
19
+ import { ensureArrangeMenu as buildArrangeMenu } from './split-view-arrange-menu.js';
20
+ import { pickerCapacity } from './split-view-picker-actions.js';
20
21
  import { reserveEmptySplit, clearEmptySplit } from './split-session-empty-pane.js';
21
22
 
22
23
  var host = null;
@@ -51,7 +52,7 @@ function dispatch(name, detail) {
51
52
  export function initPresentationRenderer(hostElement) {
52
53
  host = hostElement;
53
54
  app = document.getElementById("app");
54
- ensureArrangeMenu();
55
+ buildArrangeMenu(closePresentationArrange);
55
56
  document.addEventListener('clay-split-picker-layout', renderCurrentGeometry);
56
57
  document.addEventListener("click", onClick, true);
57
58
  document.addEventListener("keydown", onKeyDown, true);
@@ -60,7 +61,7 @@ export function initPresentationRenderer(hostElement) {
60
61
  document.addEventListener("pointercancel", onPointerCancel, true);
61
62
  document.addEventListener("lostpointercapture", onPointerCancel, true);
62
63
  window.addEventListener("blur", onWindowBlur);
63
- window.addEventListener("resize", function () { renderCurrentGeometry(); });
64
+ window.addEventListener("resize", function () { closePresentationArrange(false); renderCurrentGeometry(); });
64
65
  window.addEventListener("scroll", closePresentationArrange, true);
65
66
  if (typeof ResizeObserver === "function") {
66
67
  resizeObserver = new ResizeObserver(function () { renderCurrentGeometry(); });
@@ -136,7 +137,7 @@ export function renderPresentationView(view, tree, sessions) {
136
137
  if (app) app.classList.add("split-native-hidden");
137
138
  reconcileSplitPanes(host, panes, function (pane) { return createSplitPane(pane, function (sessionId) { dispatch("clay-split-view-close-pane", { sessionId: sessionId }); }); }, updateSplitPaneMetadata, paneRetention(host));
138
139
  Array.from(host.querySelectorAll(':scope > .split-pane .split-pane-header')).forEach(function (header) { ensureArrangeTrigger(header); });
139
- ensureArrangeMenu();
140
+ buildArrangeMenu(closePresentationArrange);
140
141
  Array.from(host.querySelectorAll(':scope > .split-pane')).forEach(function (node) {
141
142
  var target = panes.find(function (pane) { return node.dataset.splitPaneKey === pane.slug + '#' + pane.sessionId; });
142
143
  var session = target && sessions.find(function (item) { return item.id === target.sessionId; });
@@ -278,34 +279,6 @@ export function ensureArrangeTrigger(header) {
278
279
  if (pane) syncPaneHeaderState(pane);
279
280
  }
280
281
 
281
- function ensureArrangeMenu() {
282
- var menu = document.getElementById("split-view-arrange-menu");
283
- if (menu) return menu;
284
- menu = document.createElement("div"); menu.id = "split-view-arrange-menu"; menu.className = "split-view-arrange";
285
- menu.setAttribute("role", "dialog"); menu.setAttribute("aria-label", "Arrange Split view"); menu.hidden = true;
286
- var header = document.createElement("div"); header.className = "split-view-arrange-header";
287
- var title = document.createElement("strong"); title.textContent = "Arrange Split view"; header.appendChild(title);
288
- var close = document.createElement("button"); close.type = "button"; close.setAttribute("aria-label", "Close arrangement menu"); close.innerHTML = iconHtml("x"); header.appendChild(close);
289
- close.addEventListener("click", function () { closePresentationArrange(); }); menu.appendChild(header);
290
- var presets = [["balanced", "Balanced"], ["columns", "Columns"], ["rows", "Rows"], ["grid", "Grid"], ["main-left", "Main left"], ["main-right", "Main right"]];
291
- var presetGrid = document.createElement("div"); presetGrid.className = "split-view-arrange-grid";
292
- presets.forEach(function (item) { var button = document.createElement("button"); button.type = "button"; button.dataset.preset = item[0]; appendLayoutOption(button, item[1], "preset", item[0]); presetGrid.appendChild(button); });
293
- menu.appendChild(presetGrid);
294
- var positions = [["left", "Left half"], ["right", "Right half"], ["top", "Top half"], ["bottom", "Bottom half"], ["top-left", "Top left"], ["top-right", "Top right"], ["bottom-left", "Bottom left"], ["bottom-right", "Bottom right"], ["left-third", "Left third"], ["right-third", "Right third"]];
295
- var positionGrid = document.createElement("div"); positionGrid.className = "split-view-arrange-grid split-view-arrange-positions";
296
- positions.forEach(function (item) { var button = document.createElement("button"); button.type = "button"; button.dataset.position = item[0]; appendLayoutOption(button, item[1], "position", item[0]); positionGrid.appendChild(button); });
297
- menu.appendChild(positionGrid);
298
- var focusButton = document.createElement("button"); focusButton.type = "button"; focusButton.dataset.focusLayout = "true"; menu.appendChild(focusButton);
299
- var membership = document.createElement("button"); membership.type = "button"; membership.dataset.arrangeMembership = "true";
300
- membership.textContent = "Choose sessions"; menu.appendChild(membership);
301
- var context = document.createElement("div"); context.className = "split-view-arrange-context";
302
- var contextButton = document.createElement("button"); contextButton.type = "button"; contextButton.dataset.arrangeContext = "true";
303
- contextButton.setAttribute("aria-expanded", "false"); contextButton.setAttribute("aria-controls", "split-view-arrange-context-list");
304
- var contextTitle = document.createElement("span"); contextTitle.dataset.arrangeContextTitle = "true"; contextButton.appendChild(contextTitle); context.appendChild(contextButton);
305
- var contextList = document.createElement("div"); contextList.id = "split-view-arrange-context-list"; contextList.hidden = true; contextList.setAttribute("role", "listbox");
306
- context.appendChild(contextList); menu.insertBefore(context, presetGrid);
307
- document.body.appendChild(menu); return menu;
308
- }
309
282
 
310
283
  function openArrangeMenu(trigger) {
311
284
  if (!syncPaneHeaderState(trigger.closest('.split-pane'))) return;
@@ -313,7 +286,7 @@ function openArrangeMenu(trigger) {
313
286
  dispatch("clay-split-view-open-create", { anchor: trigger });
314
287
  return;
315
288
  }
316
- var menu = ensureArrangeMenu();
289
+ var menu = buildArrangeMenu(closePresentationArrange);
317
290
  if (!menu.hidden && menuTrigger === trigger) { closePresentationArrange(); return; }
318
291
  closePresentationArrange(false); menuTrigger = trigger; menu.hidden = false;
319
292
  trigger.setAttribute("aria-expanded", "true");
@@ -340,6 +313,7 @@ function openArrangeMenu(trigger) {
340
313
  Array.from(menu.querySelectorAll('[data-position*="-"]:not([data-position$="-third"])')).forEach(function (button) {
341
314
  button.disabled = count < 3; button.title = count < 3 ? "Quarter layouts require at least three sessions" : "";
342
315
  });
316
+ menu.querySelectorAll('[data-arrange-add-side]').forEach(function (button) { button.disabled = !pickerCapacity() || !!store.get('projectSplitViewPending'); });
343
317
  var rect = trigger.getBoundingClientRect(), width = Math.min(280, window.innerWidth - 16);
344
318
  menu.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)) + "px";
345
319
  menu.style.top = Math.max(8, Math.min(rect.bottom + 4, window.innerHeight - menu.offsetHeight - 8)) + "px";
@@ -362,10 +336,11 @@ function onClick(event) {
362
336
  if (trigger) { event.preventDefault(); event.stopImmediatePropagation(); openArrangeMenu(trigger); return; }
363
337
  var menu = event.target.closest('#split-view-arrange-menu');
364
338
  if (menu) {
365
- if (event.target.closest('[data-arrange-membership]')) {
339
+ var addSide = event.target.closest('[data-arrange-add-side]');
340
+ if (addSide || event.target.closest('[data-arrange-membership]')) {
366
341
  event.preventDefault(); event.stopImmediatePropagation();
367
342
  var anchor = menuTrigger; closePresentationArrange(false);
368
- if (anchor && anchor.isConnected) dispatch("clay-split-view-open-create", { anchor: anchor });
343
+ if (anchor && anchor.isConnected) dispatch("clay-split-view-open-create", { anchor: anchor, side: addSide && addSide.dataset.arrangeAddSide, origin: menu.dataset.sourceOrigin });
369
344
  return;
370
345
  }
371
346
  var contextButton = event.target.closest('[data-arrange-context]');
@@ -19,10 +19,11 @@ import { initPresentationDrag, restorePresentationDrag } from './split-view-pres
19
19
  import { detachTuiView } from './session-tui-view.js';
20
20
  import { checkComposerDeparture } from './session-rich-drafts.js';
21
21
  import { showToast } from './utils.js';
22
- import { preset, listIds, validateTree, remove, arrangeRegion } from './split-view-layout-client.js';
22
+ import { preset, listIds, validateTree, arrangeRegion } from './split-view-layout-client.js';
23
23
  import { viewIdForOrigin, updateActiveSession, exactDriverForOrigin, restorablePresentation } from './split-view-presentation-lookup.js';
24
24
  import { groupedSessionIds } from './split-group-helpers.js';
25
25
  import { handleSplitViewCreateEvent } from './split-view-create-action.js';
26
+ import { removeViewSession } from './split-view-member-actions.js';
26
27
  import { initSplitViewCreateMenu } from './split-view-create-menu.js';
27
28
  import { currentSingletonPresentation } from './split-view-singleton.js';
28
29
  var host = null;
@@ -333,6 +334,12 @@ function toggleGroupMenu(group, anchor, point) {
333
334
  }
334
335
  function runGroupAction(view, action, root) {
335
336
  closeGroupMenus(false);
337
+ if (action === 'add-pane') {
338
+ if (!setActiveView(view.id)) return;
339
+ var anchor = host && host.querySelector('.split-pane:not(.split-pane-cached) [data-action="arrange"]');
340
+ if (anchor) document.dispatchEvent(new CustomEvent('clay-split-view-open-create', { detail: { anchor: anchor, side: 'right', origin: view.activeSessionOriginId || view.sessions[0].sessionOriginId } }));
341
+ return;
342
+ }
336
343
  if (action === 'favorite' || action === 'folder') { organizeViewAction(view, action); return; }
337
344
  if (action === "rename") { startRename(root, view); return; }
338
345
  if (action === "separate") {
@@ -418,13 +425,7 @@ function onClosePane(event) {
418
425
  if (!view || !event.detail || !validPresentationClose(event.detail)) return;
419
426
  var session = (getCachedSessions() || []).find(function (item) { return item.id === Number(event.detail.sessionId); });
420
427
  if (!session || !view.sessions.some(function (item) { return item.sessionOriginId === session.sessionOriginId; })) return;
421
- try { checkMountedPaneDrafts(); } catch (error) { showToast(error.message, "warn"); return; }
422
- var origins = view.sessions.map(function (item) { return item.sessionOriginId; }).filter(function (origin) { return origin !== session.sessionOriginId; });
423
- var nextTree = remove(view.tree, session.sessionOriginId);
424
- if (!validateTree(nextTree, origins).ok) return;
425
- var nextActive = view.activeSessionOriginId === session.sessionOriginId ? origins[0] || null : view.activeSessionOriginId;
426
- mutation("project_split_view_update", { id: view.id, sessionOrigins: origins, tree: nextTree,
427
- layout: view.layout, activeSessionOriginId: nextActive }, { closedOrigin: session.sessionOriginId, activeOrigin: nextActive });
428
+ removeViewSession(view, session.sessionOriginId);
428
429
  }
429
430
  function onFocus(event) {
430
431
  var view = viewExists(store.get('projectSplitViews'), store.get('projectSplitViewActiveId'));
@@ -25,8 +25,9 @@ export function openSplitViewMenu(group, anchor, point, onAction) {
25
25
  }
26
26
  closeFolderMenu(false); closeSessionContextMenu(false);
27
27
  var menu = document.createElement('div'); menu.className = 'session-ctx-menu split-view-menu';
28
- [['favorite', view.favorite ? 'Remove from Favorites' : 'Add to Favorites', 'star'], ['folder', 'Move to folder', 'folder-input'], ['rename', 'Rename', 'pencil'], ['separate', 'Separate', 'columns-2']].forEach(function (entry) {
28
+ [['add-pane', 'Add pane', 'panel-right-open'], ['favorite', view.favorite ? 'Remove from Favorites' : 'Add to Favorites', 'star'], ['folder', 'Move to folder', 'folder-input'], ['rename', 'Rename', 'pencil'], ['separate', 'Separate', 'columns-2']].forEach(function (entry) {
29
29
  var button = document.createElement('button'); button.className = 'session-ctx-item'; button.dataset.splitViewAction = entry[0];
30
+ if (entry[0] === 'add-pane') button.disabled = !view.sessions.length || view.sessions.length + (view.workspace && view.workspace.linkedPair ? view.workspace.linkedPair.workerOrigins.length : 0) >= 8 || !!store.get('projectSplitViewPending');
30
31
  button.innerHTML = iconHtml(entry[2]) + ' <span>' + entry[1] + '</span>';
31
32
  button.addEventListener('click', function (event) {
32
33
  event.stopPropagation(); var root = currentRoot(); closeSessionContextMenu(false);