clay-server 5.2.1-beta.2 → 5.3.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 (146) hide show
  1. package/lib/clay-control-presentation-binding.js +5 -3
  2. package/lib/clay-product-control-tools.js +3 -3
  3. package/lib/clay-product-control.js +4 -2
  4. package/lib/clay-screen-controls.js +18 -3
  5. package/lib/clay-split-view-awareness.js +35 -0
  6. package/lib/clay-split-view-controls.js +122 -0
  7. package/lib/clay-split-view-input.js +20 -0
  8. package/lib/clay-split-view-render-proof.js +22 -0
  9. package/lib/pane-asset-manifest.js +63 -0
  10. package/lib/project-connection.js +19 -10
  11. package/lib/project-session-actions.js +1 -0
  12. package/lib/project-session-delete.js +2 -2
  13. package/lib/project-session-folders.js +1 -0
  14. package/lib/project-sessions.js +6 -1
  15. package/lib/project-split-view-document.js +33 -0
  16. package/lib/project-split-view-names.js +67 -0
  17. package/lib/project-split-view-organization.js +23 -0
  18. package/lib/project-split-views.js +479 -0
  19. package/lib/project-worker-conversation.js +158 -0
  20. package/lib/project-worker-creation-proposal.js +3 -4
  21. package/lib/project-worker-proposal.js +1 -1
  22. package/lib/project-worker-removal.js +101 -0
  23. package/lib/project.js +28 -1
  24. package/lib/public/app.js +32 -13
  25. package/lib/public/css/base.css +9 -0
  26. package/lib/public/css/filebrowser.css +0 -1
  27. package/lib/public/css/pane-header-controls.css +100 -0
  28. package/lib/public/css/pane.css +24 -5
  29. package/lib/public/css/popup-chat.css +123 -0
  30. package/lib/public/css/right-workbench.css +50 -18
  31. package/lib/public/css/split-view-layout.css +460 -0
  32. package/lib/public/css/split-view-worker-activity.css +97 -0
  33. package/lib/public/css/sticky-notes.css +13 -0
  34. package/lib/public/index.html +3 -3
  35. package/lib/public/modules/app-connection.js +4 -1
  36. package/lib/public/modules/app-messages.js +25 -8
  37. package/lib/public/modules/chat-render-runtime.js +45 -0
  38. package/lib/public/modules/clay-control-copy.js +3 -0
  39. package/lib/public/modules/clay-navigation-render.js +1 -1
  40. package/lib/public/modules/clay-owner-navigation.js +15 -5
  41. package/lib/public/modules/clay-split-view-screen.js +74 -0
  42. package/lib/public/modules/clay-tool-activity.js +5 -0
  43. package/lib/public/modules/command-palette.js +30 -28
  44. package/lib/public/modules/contextual-clay-session.js +26 -2
  45. package/lib/public/modules/contextual-clay.js +5 -2
  46. package/lib/public/modules/contextual-mate-sessions.js +2 -0
  47. package/lib/public/modules/generation-stop.js +7 -1
  48. package/lib/public/modules/knowledge-workbench-entry.js +43 -0
  49. package/lib/public/modules/markdown.js +11 -0
  50. package/lib/public/modules/mate-knowledge-workbench.js +2 -7
  51. package/lib/public/modules/mate-sidebar.js +1 -1
  52. package/lib/public/modules/palette-clay-conversation.js +87 -0
  53. package/lib/public/modules/palette-render-helpers.js +25 -0
  54. package/lib/public/modules/pane-activation.js +46 -7
  55. package/lib/public/modules/pane-binding.js +12 -2
  56. package/lib/public/modules/pane-bridge.js +12 -3
  57. package/lib/public/modules/pane-deferred-bootstrap.js +32 -0
  58. package/lib/public/modules/pane-feature-assets.js +35 -0
  59. package/lib/public/modules/pane-search-receipt.js +12 -0
  60. package/lib/public/modules/permission-control.js +6 -6
  61. package/lib/public/modules/popup-chat-rendering.js +244 -0
  62. package/lib/public/modules/project-activation.js +2 -2
  63. package/lib/public/modules/qrcode.js +12 -1
  64. package/lib/public/modules/right-workbench.js +68 -0
  65. package/lib/public/modules/search-hints.js +79 -0
  66. package/lib/public/modules/session-context-menu.js +35 -2
  67. package/lib/public/modules/session-folder-context.js +24 -13
  68. package/lib/public/modules/session-folder-delete.js +18 -1
  69. package/lib/public/modules/session-folder-layout.js +7 -6
  70. package/lib/public/modules/session-search-pane-state.js +30 -0
  71. package/lib/public/modules/session-search.js +53 -13
  72. package/lib/public/modules/sidebar-mobile.js +3 -3
  73. package/lib/public/modules/sidebar-projects.js +1 -0
  74. package/lib/public/modules/sidebar-session-folders-ui.js +3 -3
  75. package/lib/public/modules/sidebar-sessions.js +57 -24
  76. package/lib/public/modules/sidebar.js +20 -3
  77. package/lib/public/modules/split-group-helpers.js +1 -1
  78. package/lib/public/modules/split-group-restore.js +8 -11
  79. package/lib/public/modules/split-pair-ui.js +18 -0
  80. package/lib/public/modules/split-pane-header-state.js +45 -0
  81. package/lib/public/modules/split-pane-reconciler.js +23 -4
  82. package/lib/public/modules/split-pane-renderer.js +330 -23
  83. package/lib/public/modules/split-pane-retention.js +157 -0
  84. package/lib/public/modules/split-session-boundary.js +21 -0
  85. package/lib/public/modules/split-view-create-action.js +15 -0
  86. package/lib/public/modules/split-view-create-menu.js +83 -0
  87. package/lib/public/modules/split-view-create-request.js +23 -0
  88. package/lib/public/modules/split-view-layout-client.js +198 -0
  89. package/lib/public/modules/split-view-layout-icons.js +37 -0
  90. package/lib/public/modules/split-view-layout-scope.js +38 -0
  91. package/lib/public/modules/split-view-organization.js +73 -0
  92. package/lib/public/modules/split-view-presentation-close.js +35 -0
  93. package/lib/public/modules/split-view-presentation-drag.js +254 -0
  94. package/lib/public/modules/split-view-presentation-drop.js +57 -0
  95. package/lib/public/modules/split-view-presentation-lookup.js +44 -0
  96. package/lib/public/modules/split-view-presentation-renderer.js +494 -0
  97. package/lib/public/modules/split-view-presentations.js +495 -0
  98. package/lib/public/modules/split-view-shell-controls.js +28 -0
  99. package/lib/public/modules/split-view-sidebar-menu.js +42 -0
  100. package/lib/public/modules/split-view-sidebar-renderer.js +30 -0
  101. package/lib/public/modules/split-view-singleton.js +21 -0
  102. package/lib/public/modules/split-view-worker-activity.js +94 -0
  103. package/lib/public/modules/split-view-worker-controls.js +30 -0
  104. package/lib/public/modules/split-view-worker-removal.js +137 -0
  105. package/lib/public/modules/split-view-worker-scroll.js +57 -0
  106. package/lib/public/modules/split-view-worker-transcript.js +46 -0
  107. package/lib/public/modules/split-view-workers.js +352 -0
  108. package/lib/public/modules/split-view.js +61 -22
  109. package/lib/public/modules/sticky-notes.js +9 -2
  110. package/lib/public/modules/tools.js +25 -5
  111. package/lib/public/modules/workbench-layout-preference.js +76 -0
  112. package/lib/public/pane-assets/app-OJEQXBBS.js +1 -0
  113. package/lib/public/pane-assets/chunk-4SMIVAB4.js +1 -0
  114. package/lib/public/pane-assets/chunk-623V5CEO.js +1 -0
  115. package/lib/public/pane-assets/chunk-6SM2WQJX.js +1 -0
  116. package/lib/public/pane-assets/chunk-7H36SUZA.js +2 -0
  117. package/lib/public/pane-assets/chunk-DGOFGEUK.js +1 -0
  118. package/lib/public/pane-assets/chunk-GML2R5JE.js +167 -0
  119. package/lib/public/pane-assets/chunk-I57URYSL.js +1 -0
  120. package/lib/public/pane-assets/chunk-I5P3MWQG.js +1 -0
  121. package/lib/public/pane-assets/chunk-UVGOPFEX.js +2 -0
  122. package/lib/public/pane-assets/manifest.json +551 -0
  123. package/lib/public/pane-assets/style-GN7R6XKV.css +1 -0
  124. package/lib/public/style.css +5 -1
  125. package/lib/server-owner-ui.js +6 -2
  126. package/lib/server-owner-view-observation.js +34 -0
  127. package/lib/server-palette.js +1 -0
  128. package/lib/server-pane-bootstrap.js +24 -0
  129. package/lib/server-settings.js +2 -0
  130. package/lib/server-static-assets.js +52 -0
  131. package/lib/server-workbench-layout-settings.js +56 -0
  132. package/lib/server.js +6 -45
  133. package/lib/session-activity-state.js +28 -0
  134. package/lib/session-list-delivery.js +17 -0
  135. package/lib/session-pair-turn-control.js +1 -6
  136. package/lib/session-search.js +3 -0
  137. package/lib/session-split-group-anchors.js +2 -2
  138. package/lib/session-split-group-roles.js +5 -8
  139. package/lib/session-split-group-v2-store.js +0 -3
  140. package/lib/sessions.js +16 -6
  141. package/lib/split-view-layout.js +132 -0
  142. package/lib/users-workbench-layout-preferences.js +33 -0
  143. package/lib/users.js +3 -0
  144. package/lib/workspace-query-service.js +1 -5
  145. package/lib/ws-schema.js +15 -0
  146. package/package.json +6 -3
@@ -1,9 +1,8 @@
1
1
  // Pure projection of sidebar units into folder sections. No DOM, no store.
2
2
  //
3
3
  // A unit is one organizable root: a standalone session or a Driver with all of
4
- // its Workers. { key, title, createdAt, lastActivity, item }. Units whose key
5
- // is null (loop runs, split groups, orphan Workers) cannot be filed, so they
6
- // always live with the ordinary, unfiled sessions.
4
+ // its Workers, or a saved presentation view. Saved views own their folder and
5
+ // Favorite metadata. Unkeyed legacy groups and loop runs remain Unfiled.
7
6
 
8
7
  export var FAVORITES_ID = "favorites";
9
8
  export var UNFILED_ID = "unfiled";
@@ -48,7 +47,8 @@ export function isFavorite(state, key) {
48
47
 
49
48
  export function containerOf(state, unit) {
50
49
  if (unit.key === null) return UNFILED_ID;
51
- var id = state.assignments[unit.key];
50
+ var group = unit.item && unit.item.type === "split-group" && unit.item.group && unit.item.group.presentation ? unit.item.group : null;
51
+ var id = group ? group.folderId : state.assignments[unit.key];
52
52
  for (var i = 0; i < state.folders.length; i++) if (state.folders[i].id === id) return id;
53
53
  return UNFILED_ID;
54
54
  }
@@ -66,7 +66,8 @@ export function buildLayout(units, state, options) {
66
66
  var ordinary = [];
67
67
  for (var i = 0; i < units.length; i++) {
68
68
  // The same unit may appear twice: once as a favorite, once in its folder.
69
- if (isFavorite(state, units[i].key)) favorites.push(units[i]);
69
+ var group = units[i].item && units[i].item.type === "split-group" && units[i].item.group && units[i].item.group.presentation ? units[i].item.group : null;
70
+ if (group ? group.favorite === true : isFavorite(state, units[i].key)) favorites.push(units[i]);
70
71
  ordinary.push(units[i]);
71
72
  }
72
73
  var result = {
@@ -117,7 +118,7 @@ export function containerKeys(layout, containerId) {
117
118
  if (layout.sections[i].id === containerId) list = layout.sections[i].units;
118
119
  }
119
120
  }
120
- return list.filter(function (u) { return u.key !== null; }).map(function (u) { return u.key; });
121
+ return list.filter(function (u) { return u.key !== null && !(u.item && u.item.type === "split-group" && u.item.group && u.item.group.presentation); }).map(function (u) { return u.key; });
121
122
  }
122
123
 
123
124
  export var SORT_OPTIONS = [
@@ -0,0 +1,30 @@
1
+ import { store } from './store.js';
2
+ import { paneMessageBinding } from './pane-binding.js';
3
+ import { acceptsPaneSearchResults } from './pane-search-receipt.js';
4
+
5
+ function publishPaneSearchStatus(query, status) {
6
+ if (!store.get('paneMode') || window.parent === window) return;
7
+ window.parent.postMessage({ type: "clay-pane-search-status", paneBinding: paneMessageBinding(), query: query,
8
+ status: status, open: store.get('paneSearchOpen') === true }, window.location.origin);
9
+ }
10
+
11
+ function updatePaneSearchStatus(countElement, query, status) {
12
+ if (countElement) countElement.textContent = status;
13
+ publishPaneSearchStatus(query, status);
14
+ }
15
+
16
+ function beginPaneSearch(query) {
17
+ var binding = paneMessageBinding(), epoch = (Number(store.get('paneSearchEpoch')) || 0) + 1;
18
+ store.set({ paneSearchEpoch: epoch, paneSearchReceipt: binding ? { binding: binding, epoch: epoch, query: query } : null });
19
+ }
20
+
21
+ function endPaneSearch() {
22
+ store.set({ paneSearchOpen: false, paneSearchReceipt: null, paneSearchEpoch: (Number(store.get('paneSearchEpoch')) || 0) + 1 });
23
+ }
24
+
25
+ function acceptsSessionSearchResults(state, msg, query) {
26
+ if (state.paneMode && (!state.paneSearchOpen || !acceptsPaneSearchResults(state, msg))) return false;
27
+ return msg.query === query;
28
+ }
29
+
30
+ export { updatePaneSearchStatus, beginPaneSearch, endPaneSearch, acceptsSessionSearchResults };
@@ -1,6 +1,9 @@
1
1
  // --- In-session search (Cmd+F / Ctrl+F) ---
2
2
  import { escapeHtml } from './utils.js';
3
3
  import { iconHtml, refreshIcons } from './icons.js';
4
+ import { store } from './store.js';
5
+ import { paneMessageBinding } from './pane-binding.js';
6
+ import { updatePaneSearchStatus, beginPaneSearch, endPaneSearch, acceptsSessionSearchResults } from './session-search-pane-state.js';
4
7
 
5
8
  var ctx = null;
6
9
  var searchBarEl = null;
@@ -19,9 +22,20 @@ var serverHits = []; // full history hits from server
19
22
  var serverTotal = 0;
20
23
  var pendingScrollTarget = null;
21
24
 
25
+ function setSearchStatus(text) {
26
+ updatePaneSearchStatus(matchCountEl, currentQuery, text);
27
+ }
28
+
22
29
  function initSessionSearch(context) {
23
30
  ctx = context;
24
31
  createSearchBar();
32
+ var searchBinding = paneMessageBinding();
33
+ store.subscribe(function () {
34
+ var nextBinding = paneMessageBinding();
35
+ if (searchBinding === nextBinding) return;
36
+ searchBinding = nextBinding;
37
+ if (store.get('paneSearchOpen') || currentQuery || matches.length || serverHits.length) closeSearch();
38
+ });
25
39
  document.addEventListener("keydown", function (e) {
26
40
  var isMod = e.metaKey || e.ctrlKey;
27
41
  if (isMod && e.key === "f") {
@@ -90,11 +104,16 @@ function createSearchBar() {
90
104
  }
91
105
 
92
106
  function isSearchOpen() {
107
+ if (store.get('paneMode')) return store.get('paneSearchOpen') === true;
93
108
  return searchBarEl && !searchBarEl.classList.contains("hidden");
94
109
  }
95
110
 
96
111
  function openSearch(prefill) {
97
112
  if (!searchBarEl) return;
113
+ if (store.get('paneMode') && window.parent !== window) {
114
+ window.parent.postMessage({ type: "clay-pane-search-open", paneBinding: paneMessageBinding() }, window.location.origin);
115
+ return;
116
+ }
98
117
  searchBarEl.classList.remove("hidden");
99
118
  var btn = document.getElementById("find-in-session-btn");
100
119
  if (btn) btn.classList.add("active");
@@ -111,6 +130,7 @@ function openSearch(prefill) {
111
130
 
112
131
  function closeSearch() {
113
132
  if (!searchBarEl) return;
133
+ endPaneSearch();
114
134
  searchBarEl.classList.add("hidden");
115
135
  var btn = document.getElementById("find-in-session-btn");
116
136
  if (btn) btn.classList.remove("active");
@@ -121,7 +141,27 @@ function closeSearch() {
121
141
  currentMatchIndex = -1;
122
142
  serverHits = [];
123
143
  serverTotal = 0;
124
- matchCountEl.textContent = "";
144
+ setSearchStatus("");
145
+ }
146
+
147
+ export function controlPaneSearch(action, query) {
148
+ if (!store.get('paneMode')) return false;
149
+ if (action === "close") { closeSearch(); return true; }
150
+ if (action === "open") {
151
+ store.set({ paneSearchOpen: true });
152
+ searchInputEl.value = String(query || "");
153
+ performSearch(searchInputEl.value);
154
+ return true;
155
+ }
156
+ if (!store.get('paneSearchOpen')) return false;
157
+ if (action === "query") {
158
+ searchInputEl.value = String(query || "");
159
+ performSearch(searchInputEl.value);
160
+ return true;
161
+ }
162
+ if (action === "next") { goToNextMatch(); return true; }
163
+ if (action === "previous") { goToPrevMatch(); return true; }
164
+ return false;
125
165
  }
126
166
 
127
167
  function toggleSearch() {
@@ -140,9 +180,10 @@ function performSearch(query) {
140
180
  serverHits = [];
141
181
  serverTotal = 0;
142
182
  currentQuery = query.trim();
183
+ if (store.get('paneMode')) beginPaneSearch(currentQuery);
143
184
 
144
185
  if (!currentQuery) {
145
- matchCountEl.textContent = "";
186
+ setSearchStatus("");
146
187
  return;
147
188
  }
148
189
 
@@ -171,16 +212,15 @@ function highlightLoadedMessages() {
171
212
  if (matches.length > 0) {
172
213
  currentMatchIndex = 0;
173
214
  setActiveMatch(0);
174
- matchCountEl.textContent = "1 / " + matches.length;
215
+ setSearchStatus("1 / " + matches.length);
175
216
  } else {
176
- matchCountEl.textContent = "Searching...";
217
+ setSearchStatus("Searching...");
177
218
  }
178
219
  }
179
220
 
180
221
  // Handle server response with full-history search results
181
222
  function handleFindInSessionResults(msg) {
182
- if (!isSearchOpen()) return;
183
- if (msg.query !== currentQuery) return; // stale
223
+ if (!isSearchOpen() || !acceptsSessionSearchResults(store.snap(), msg, currentQuery)) return;
184
224
 
185
225
  serverHits = msg.hits || [];
186
226
  serverTotal = msg.total || 0;
@@ -191,13 +231,13 @@ function handleFindInSessionResults(msg) {
191
231
  clearHighlights();
192
232
  highlightLoadedMessages();
193
233
  if (matches.length > 0) {
194
- matchCountEl.textContent = "1 / " + matches.length + " (" + serverHits.length + " total)";
234
+ setSearchStatus("1 / " + matches.length + " (" + serverHits.length + " total)");
195
235
  } else {
196
- matchCountEl.textContent = serverHits.length + " matches";
236
+ setSearchStatus(serverHits.length + " matches");
197
237
  }
198
238
  buildTimeline();
199
239
  } else {
200
- matchCountEl.textContent = "No results";
240
+ setSearchStatus("No results");
201
241
  }
202
242
  }
203
243
 
@@ -441,16 +481,16 @@ function goToNextMatch() {
441
481
  if (matches.length === 0) return;
442
482
  currentMatchIndex = (currentMatchIndex + 1) % matches.length;
443
483
  setActiveMatch(currentMatchIndex);
444
- matchCountEl.textContent = (currentMatchIndex + 1) + " / " + matches.length +
445
- (serverHits.length > matches.length ? " (" + serverHits.length + " total)" : "");
484
+ setSearchStatus((currentMatchIndex + 1) + " / " + matches.length +
485
+ (serverHits.length > matches.length ? " (" + serverHits.length + " total)" : ""));
446
486
  }
447
487
 
448
488
  function goToPrevMatch() {
449
489
  if (matches.length === 0) return;
450
490
  currentMatchIndex = (currentMatchIndex - 1 + matches.length) % matches.length;
451
491
  setActiveMatch(currentMatchIndex);
452
- matchCountEl.textContent = (currentMatchIndex + 1) + " / " + matches.length +
453
- (serverHits.length > matches.length ? " (" + serverHits.length + " total)" : "");
492
+ setSearchStatus((currentMatchIndex + 1) + " / " + matches.length +
493
+ (serverHits.length > matches.length ? " (" + serverHits.length + " total)" : ""));
454
494
  }
455
495
 
456
496
  export { initSessionSearch, openSearch, closeSearch, toggleSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended };
@@ -47,7 +47,7 @@ import { ordinarySessionListSessions } from './session-list-visibility.js';
47
47
  import { formatSessionActivity } from './session-activity-tooltip.js';
48
48
  import { bindSessionTitleMarquee } from './session-title-marquee.js';
49
49
  import { summarizeSidebarItems } from './sidebar-session-counts.js';
50
- import { cancelSessionInlineRename, closeSessionContextMenu } from './session-context-menu.js';
50
+ import { cancelSessionInlineRename, reconcileSessionContextMenu } from './session-context-menu.js';
51
51
 
52
52
  // --- Mobile state ---
53
53
  var mobileChatSheetOpen = false;
@@ -447,7 +447,7 @@ function createMobileSessionItem(s, options) {
447
447
  el.addEventListener("contextmenu", function (event) {
448
448
  event.preventDefault();
449
449
  event.stopPropagation();
450
- openSessionContextMenu(el, s);
450
+ openSessionContextMenu(el, s, event.clientX || event.clientY ? { x: event.clientX, y: event.clientY } : null);
451
451
  });
452
452
 
453
453
  (function (session, worker) {
@@ -775,7 +775,6 @@ export function refreshMobileChatSheet() {
775
775
  var sessionListEl = sheet.querySelector(".mobile-chat-session-list");
776
776
  if (!sessionListEl) return;
777
777
 
778
- closeSessionContextMenu(false);
779
778
  cancelSessionInlineRename(false);
780
779
 
781
780
  // Update chips: active state and processing dots
@@ -815,6 +814,7 @@ export function refreshMobileChatSheet() {
815
814
  renderMobileSessionsInto(sessionListEl);
816
815
 
817
816
  refreshIcons();
817
+ reconcileSessionContextMenu();
818
818
  }
819
819
 
820
820
  function renderSheetMateProfile(listEl) {
@@ -1020,6 +1020,7 @@ function showWorktreeModal(parentSlug, parentName) {
1020
1020
  export function renderIconStrip(projects, currentSlug) {
1021
1021
  cachedProjectList = projects;
1022
1022
  cachedCurrentSlug = currentSlug;
1023
+ if (store.get('paneMode')) return;
1023
1024
 
1024
1025
  var container = document.getElementById("icon-strip-projects");
1025
1026
  if (!container) return;
@@ -65,7 +65,7 @@ export function unitsFromItems(items) {
65
65
  var item = items[i];
66
66
  var data = item.type === "session" || item.type === "driver-hierarchy" ? item.data : null;
67
67
  units.push({
68
- key: data && typeof data.id === "number" ? data.id : null,
68
+ key: item.type === "split-group" && item.group && item.group.presentation ? "split-view:" + item.group.id : data && typeof data.id === "number" ? data.id : null,
69
69
  title: data ? data.title : (item.type === "split-group" ? (item.group && item.group.name) : (item.type === "loop" ? item.loopId : "")),
70
70
  createdAt: data ? data.createdAt || 0 : 0,
71
71
  lastActivity: data ? (data.lastActivity || 0) : (item.lastActivity || 0),
@@ -275,9 +275,9 @@ function renderUnit(unit, containerId, ctx) {
275
275
  wrapper.className = "session-folder-unit";
276
276
  if (unit.key) {
277
277
  wrapper.dataset.unitKey = String(unit.key);
278
- if (ctx.surface === "desktop") wireUnitDrop(wrapper, containerId, unit.key);
278
+ if (ctx.surface === "desktop" && typeof unit.key === "number") wireUnitDrop(wrapper, containerId, unit.key);
279
279
  }
280
- wrapper.appendChild(ctx.renderItem(unit.item));
280
+ wrapper.appendChild(ctx.renderItem(unit.item, containerId, ctx.surface));
281
281
  return wrapper;
282
282
  }
283
283
 
@@ -13,6 +13,8 @@ import { showConfirm } from './app-misc.js';
13
13
  import { getUpcomingSchedules } from './scheduler.js';
14
14
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
15
15
  import { openGroup, openWorkerWithDriver, separateGroup } from './split-view.js';
16
+ import { presentationViewForSidebar } from './split-view-presentations.js';
17
+ import { renderPresentationSplitViewItem } from './split-view-sidebar-renderer.js';
16
18
  import { groupedSessionIds } from './split-group-helpers.js';
17
19
  import { resolvePreferredVendor } from './default-vendor.js';
18
20
  import {
@@ -21,6 +23,7 @@ import {
21
23
  } from './sidebar-session-folders-ui.js';
22
24
  import { renderFolderToolbar, captureFolderInputs, folderToolbarNeedsRender } from './session-folder-toolbar.js';
23
25
  import { captureFolderCollapseAnimations } from './session-folder-collapse.js';
26
+ import { closeFolderMenu, reconcileFolderMenu } from './session-folder-context.js';
24
27
  import { getListSearchQuery, getListSearchMatchIds, applyListSearchResults, filterItemsBySearch } from './session-list-search.js';
25
28
  import { expectCreatedSession } from './session-create.js';
26
29
  import { isMateWorkspace } from './project-mate-navigation.js';
@@ -39,6 +42,8 @@ import {
39
42
  cancelSessionInlineRename,
40
43
  closeSessionContextMenu,
41
44
  mountSessionContextMenu,
45
+ positionSessionContextMenu,
46
+ reconcileSessionContextMenu,
42
47
  startSessionInlineRename
43
48
  } from './session-context-menu.js';
44
49
 
@@ -203,6 +208,8 @@ export function initSidebarSessions() {
203
208
  var b = prev.sessionCreate;
204
209
  if ((!a !== !b || (a && b && a.folderId !== b.folderId)) && getSessionListEl()) renderSessionList(null);
205
210
  if (state.splitPanes !== prev.splitPanes) renderSessionList(null);
211
+ if (state.projectSplitViews !== prev.projectSplitViews || state.projectSplitViewActiveId !== prev.projectSplitViewActiveId ||
212
+ state.projectSplitViewError !== prev.projectSplitViewError || state.projectSplitViewPending !== prev.projectSplitViewPending) renderSessionList(null);
206
213
  });
207
214
 
208
215
  // --- Resume session picker ---
@@ -234,8 +241,17 @@ export function getExpandedLoopRuns() {
234
241
 
235
242
  // --- Context menu ---
236
243
 
237
- function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
244
+ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData, point) {
238
245
  closeSessionContextMenu(false);
246
+ closeFolderMenu(false);
247
+ var origin = sessionData && sessionData.sessionOriginId, owner = sessionData && sessionData.ownerId;
248
+ var rowSelector = anchorBtn.classList.contains('mobile-session-item') ? '.mobile-session-item' : '.session-item';
249
+ var wasFavorite = isFavoriteSession(sessionData);
250
+ function currentAnchor() {
251
+ var live = cachedSessions.find(function (item) { return item.id === sessionId && item.sessionOriginId === origin && item.ownerId === owner && item.title === title; });
252
+ if (!live || isFavoriteSession(live) !== wasFavorite) return null;
253
+ return anchorBtn.isConnected ? anchorBtn : document.querySelector(rowSelector + '[data-session-id="' + sessionId + '"]');
254
+ }
239
255
 
240
256
  var menu = document.createElement("div");
241
257
  menu.className = "session-ctx-menu";
@@ -268,8 +284,8 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
268
284
  renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
269
285
  renameItem.addEventListener("click", function (e) {
270
286
  e.stopPropagation();
271
- closeSessionContextMenu(false);
272
- startSessionInlineRename(anchorBtn, sessionData);
287
+ var row = currentAnchor(); closeSessionContextMenu(false);
288
+ if (row) startSessionInlineRename(row, sessionData);
273
289
  });
274
290
  menu.appendChild(renameItem);
275
291
 
@@ -305,26 +321,13 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
305
321
  }
306
322
 
307
323
  document.body.appendChild(menu);
308
- mountSessionContextMenu(menu, anchorBtn, sessionId);
324
+ mountSessionContextMenu(menu, anchorBtn, sessionId, { resolveAnchor: currentAnchor });
309
325
  refreshIcons();
310
-
311
- // Position: fixed relative to the anchor button
312
- requestAnimationFrame(function () {
313
- var btnRect = anchorBtn.getBoundingClientRect();
314
- menu.style.position = "fixed";
315
- menu.style.top = (btnRect.bottom + 2) + "px";
316
- menu.style.right = (window.innerWidth - btnRect.right) + "px";
317
- menu.style.left = "auto";
318
- // If menu overflows below viewport, flip up
319
- var menuRect = menu.getBoundingClientRect();
320
- if (menuRect.bottom > window.innerHeight - 8) {
321
- menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
322
- }
323
- });
326
+ positionSessionContextMenu(menu, anchorBtn, point);
324
327
  }
325
328
 
326
- export function openSessionContextMenu(anchor, session) {
327
- showSessionCtxMenu(anchor, session.id, session.title, session.cliSessionId, session);
329
+ export function openSessionContextMenu(anchor, session, point) {
330
+ showSessionCtxMenu(anchor, session.id, session.title, session.cliSessionId, session, point);
328
331
  }
329
332
 
330
333
  function showSplitGroupCtxMenu(anchor, group) {
@@ -867,9 +870,14 @@ function renderSessionItem(s, options) {
867
870
  return function(e) {
868
871
  e.preventDefault();
869
872
  e.stopPropagation();
870
- showSessionCtxMenu(anchor, id, title, cliSid, sData);
873
+ showSessionCtxMenu(anchor, id, title, cliSid, sData, e.clientX || e.clientY ? { x: e.clientX, y: e.clientY } : null);
871
874
  };
872
875
  })(s.id, s.title, s.cliSessionId, el, s));
876
+ el.addEventListener('keydown', function (event) {
877
+ if (event.key !== 'ContextMenu' && !(event.shiftKey && event.key === 'F10')) return;
878
+ if (event.target.closest('input, textarea, [contenteditable="true"]')) return;
879
+ event.preventDefault(); event.stopPropagation(); showSessionCtxMenu(el, s.id, s.title, s.cliSessionId, s);
880
+ });
873
881
 
874
882
  // Unread badge
875
883
  var unreadBadge = document.createElement("span");
@@ -975,6 +983,9 @@ function sessionRenderFingerprint(sessions) {
975
983
  }
976
984
 
977
985
  export function renderSessionList(sessions) {
986
+ // Pane activation consumes this exact list, but its sidebar is parent-owned.
987
+ // Avoid building every hidden session/folder/Worker row in each iframe.
988
+ if (store.get('paneMode')) { if (sessions) cachedSessions = sessions; return; }
978
989
  if (sessions) {
979
990
  // A reconnect and a streaming turn both re-send this list unchanged;
980
991
  // rebuilding the sidebar for an identical frame is what made it flicker.
@@ -983,7 +994,6 @@ export function renderSessionList(sessions) {
983
994
  cachedSessions = sessions;
984
995
  }
985
996
 
986
- closeSessionContextMenu(false);
987
997
  cancelSessionInlineRename(false);
988
998
 
989
999
  // Remember the inline folder draft before either surface is rebuilt: the mobile
@@ -1007,6 +1017,26 @@ export function renderSessionList(sessions) {
1007
1017
  hierarchySessionIds.forEach(function (id) { groupedIds.delete(id); });
1008
1018
  var sessionsById = new Map();
1009
1019
  for (var si = 0; si < visibleSessions.length; si++) sessionsById.set(visibleSessions[si].id, visibleSessions[si]);
1020
+ var presentationItems = [];
1021
+ var presentationViews = store.get('projectSplitViews') || [];
1022
+ for (var pvi = 0; pvi < presentationViews.length; pvi++) {
1023
+ var presentationView = presentationViewForSidebar(presentationViews[pvi].id);
1024
+ if (!presentationView || !Array.isArray(presentationView.sessions) || presentationView.sessions.length < 2 || presentationView.sessions.length > 8) continue;
1025
+ var presentationMembers = [], validPresentation = true, presentationActivity = 0;
1026
+ for (var pmi = 0; pmi < presentationView.sessions.length; pmi++) {
1027
+ var presentationRef = presentationView.sessions[pmi], presentationSession = null;
1028
+ for (var psi = 0; psi < visibleSessions.length; psi++) {
1029
+ if (visibleSessions[psi].id === presentationRef.id && visibleSessions[psi].sessionOriginId === presentationRef.sessionOriginId) presentationSession = visibleSessions[psi];
1030
+ }
1031
+ if (!presentationSession || presentationSession.sessionRole === "worker" || presentationSession.isMate || presentationSession.isTui ||
1032
+ groupedIds.has(presentationSession.id)) { validPresentation = false; break; }
1033
+ presentationMembers.push(presentationSession); presentationActivity = Math.max(presentationActivity, presentationSession.lastActivity || 0);
1034
+ }
1035
+ if (!validPresentation) continue;
1036
+ for (var pgm = 0; pgm < presentationMembers.length; pgm++) groupedIds.add(presentationMembers[pgm].id);
1037
+ presentationItems.push({ type: "split-group", group: Object.assign({ presentation: true }, presentationView), members: presentationMembers,
1038
+ lastActivity: presentationActivity });
1039
+ }
1010
1040
  var loopGroups = {}; // groupKey -> [sessions]
1011
1041
  var normalSessions = [];
1012
1042
  for (var i = 0; i < visibleSessions.length; i++) {
@@ -1056,6 +1086,7 @@ export function renderSessionList(sessions) {
1056
1086
  lastActivity: splitLastActivity,
1057
1087
  });
1058
1088
  }
1089
+ for (var pitem = 0; pitem < presentationItems.length; pitem++) items.push(presentationItems[pitem]);
1059
1090
  var groupKeys = Object.keys(loopGroups);
1060
1091
  for (var k = 0; k < groupKeys.length; k++) {
1061
1092
  var gk = groupKeys[k];
@@ -1079,9 +1110,9 @@ export function renderSessionList(sessions) {
1079
1110
  searching: matchIds !== null,
1080
1111
  matchIds: matchIds,
1081
1112
  sessions: cachedSessions,
1082
- renderItem: function (it) {
1113
+ renderItem: function (it, containerId, surface) {
1083
1114
  if (it.type === "loop") return renderLoopGroup(it.loopId, it.children, it.groupKey) || document.createElement("div");
1084
- if (it.type === "split-group") return renderSplitGroupItem(it.group, it.members);
1115
+ if (it.type === "split-group") return it.group.presentation ? renderPresentationSplitViewItem(it.group, it.members, renderSessionItem, containerId, surface) : renderSplitGroupItem(it.group, it.members);
1085
1116
  if (it.type === "driver-hierarchy") return renderDesktopDriverHierarchy(it.root, renderSessionItem, rerender, matchIds);
1086
1117
  if (it.type === "orphan-workers") return document.createElement("div");
1087
1118
  return renderSessionItem(it.data);
@@ -1101,6 +1132,8 @@ export function renderSessionList(sessions) {
1101
1132
  renderOrdinarySections(layout, folderCtx, regularContainer);
1102
1133
  getSessionListEl().appendChild(regularContainer);
1103
1134
  refreshIcons();
1135
+ reconcileSessionContextMenu();
1136
+ reconcileFolderMenu();
1104
1137
  if (updatePageTitle) updatePageTitle();
1105
1138
  // Recorded from the sessions actually rendered, so a null-argument re-render
1106
1139
  // driven by other state refreshes the fingerprint rather than stranding it.
@@ -63,11 +63,27 @@ export function closeSidebar() {
63
63
  }
64
64
 
65
65
  export function toggleSidebarCollapse() {
66
- if (!ctx) return;
67
- var layout = ctx.$("layout");
66
+ var layout = document.getElementById("layout");
67
+ if (!layout && ctx) layout = ctx.$("layout");
68
+ if (!layout) return;
68
69
  var collapsed = layout.classList.toggle("sidebar-collapsed");
69
70
  try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
70
- setTimeout(function () { syncResizeHandles(); }, 210);
71
+ updateSidebarToggleState(collapsed);
72
+ if (ctx) setTimeout(function () { syncResizeHandles(); }, 210);
73
+ }
74
+
75
+ function updateSidebarToggleState(collapsed) {
76
+ var buttons = document.querySelectorAll("#sidebar-toggle-btn, #sidebar-expand-btn, #mate-sidebar-toggle-btn, #sidebar-shell-expand-btn");
77
+ for (var i = 0; i < buttons.length; i++) {
78
+ buttons[i].setAttribute("aria-expanded", collapsed ? "false" : "true");
79
+ buttons[i].title = collapsed ? "Open sidebar" : "Collapse sidebar";
80
+ buttons[i].setAttribute("aria-label", collapsed ? "Open sidebar" : "Collapse sidebar");
81
+ if (buttons[i].id === "sidebar-shell-expand-btn") {
82
+ var splitVisible = document.getElementById("split-host");
83
+ var desktop = !window.matchMedia || window.matchMedia("(min-width: 769px)").matches;
84
+ buttons[i].hidden = !collapsed || !desktop || !splitVisible || !splitVisible.classList.contains("visible");
85
+ }
86
+ }
71
87
  }
72
88
 
73
89
  export function initSidebar(_ctx) {
@@ -106,6 +122,7 @@ export function initSidebar(_ctx) {
106
122
  ctx.$("layout").classList.add("sidebar-collapsed");
107
123
  }
108
124
  } catch (e) {}
125
+ updateSidebarToggleState(ctx.$("layout").classList.contains("sidebar-collapsed"));
109
126
 
110
127
  if (ctx.newSessionBtn) {
111
128
  ctx.newSessionBtn.addEventListener("click", function () {
@@ -39,7 +39,7 @@ export function splitGroupRoles(group) {
39
39
  var driverId = pair.driverId;
40
40
  var workerIds = hasWorkerIds ? (Array.isArray(pair.workerIds) ? pair.workerIds.slice() : []) :
41
41
  (pair.workerId !== undefined ? [pair.workerId] : []);
42
- if (!Number.isInteger(driverId) || workerIds.length < 1 || workerIds.length > 2) return null;
42
+ if (!Number.isInteger(driverId) || workerIds.length < 1) return null;
43
43
  if (group.members.indexOf(driverId) === -1) return null;
44
44
  var ordered = [driverId];
45
45
  for (var i = 0; i < workerIds.length; i++) {
@@ -26,7 +26,7 @@ export function reconcileSplitIdentity(state, previous, host) {
26
26
 
27
27
  // The initial owner response can arrive after history and split metadata.
28
28
  // Wait for it so identity cleanup cannot discard panes we just restored.
29
- export function restoreSplitGroup(openGroup) {
29
+ export function restoreSplitGroup() {
30
30
  var state = store.snap();
31
31
  if (state.authIdentityLoaded === false || state.isMultiUserMode && !state.myUserId) return;
32
32
  if (!isCurrentProjectSessionReady(state, getWs())) return;
@@ -36,6 +36,10 @@ export function restoreSplitGroup(openGroup) {
36
36
  return;
37
37
  }
38
38
  if (state.paneMode) return;
39
+ var activePresentation = (state.projectSplitViews || []).find(function (view) { return view.id === state.projectSplitViewActiveId; });
40
+ if (activePresentation && activePresentation.sessions.some(function (session) {
41
+ return session.id === state.activeSessionId || session.sessionOriginId === state.projectSplitViewFocusedOrigin;
42
+ })) return;
39
43
  if (split) {
40
44
  var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
41
45
  if (reconciliation.action === "clear") {
@@ -55,14 +59,7 @@ export function restoreSplitGroup(openGroup) {
55
59
  }
56
60
  return;
57
61
  }
58
- var activeId = store.get('activeSessionId');
59
- if (!activeId) return;
60
- var groups = store.get('splitGroups') || [];
61
- for (var i = 0; i < groups.length; i++) {
62
- var members = groups[i].members || [];
63
- if (members.indexOf(activeId) !== -1) {
64
- openGroup(groups[i]);
65
- return;
66
- }
67
- }
62
+ // An execution group is not a presentation choice. Restore an explicitly
63
+ // saved legacy split above, but keep an ordinary selected Driver in its
64
+ // single Pane even when it owns Workers. Worker Controls opens the pair.
68
65
  }
@@ -325,3 +325,21 @@ export function syncPairChrome(host, split) {
325
325
  livePane.querySelector(".split-pane-header").appendChild(live);
326
326
  }
327
327
  }
328
+
329
+ // A reconciled pane may move from a legacy Worker pair into a presentation
330
+ // Split view. Remove pair-only role and runtime chrome before refreshing the
331
+ // new session metadata; legacy rendering adds it again immediately afterward.
332
+ export function clearPanePairChrome(paneEl) {
333
+ if (!paneEl) return;
334
+ paneEl.classList.remove("split-pane-role-driver");
335
+ paneEl.classList.remove("split-pane-role-worker");
336
+ if (paneEl.style.removeProperty) {
337
+ paneEl.style.removeProperty("--split-pane-order");
338
+ paneEl.style.removeProperty("--split-worker-row");
339
+ }
340
+ [".split-pair-role", ".split-pair-live", ".split-pair-set-driver", ".split-worker-runtime"].forEach(function (selector) {
341
+ Array.from(paneEl.querySelectorAll(selector)).forEach(function (item) { item.remove(); });
342
+ });
343
+ var header = paneEl.querySelector(".split-pane-header");
344
+ if (header) header.classList.remove("has-worker-runtime");
345
+ }
@@ -0,0 +1,45 @@
1
+ import { store } from './store.js';
2
+ import { getCachedSessions } from './sidebar-sessions.js';
3
+ import { livePaneBinding, paneTarget, samePaneTarget } from './pane-binding.js';
4
+
5
+ function exactReadyPane(paneEl, frame) {
6
+ if (!paneEl || !frame || !frame.clayPane || !store.get('connected') || frame.dataset.requestedSessionId) return false;
7
+ var live = frame.clayPane.identity();
8
+ var sessionId = Number(paneEl.dataset.sessionId);
9
+ var target = live && paneTarget(store.snap(), getCachedSessions(), live.sessionId);
10
+ return !!(live && live.ready && sessionId === live.sessionId && Number(frame.dataset.sessionId) === live.sessionId &&
11
+ live.binding && live.binding.sessionOriginId === frame.dataset.sessionOriginId &&
12
+ samePaneTarget(target, live.binding) && livePaneBinding(frame));
13
+ }
14
+
15
+ export function syncPaneHeaderState(paneEl) {
16
+ var header = paneEl && paneEl.querySelector('.split-pane-header');
17
+ var frame = paneEl && paneEl.querySelector('.split-pane-frame');
18
+ if (!header || !frame) return false;
19
+ var ready = exactReadyPane(paneEl, frame);
20
+ header.setAttribute('aria-busy', ready ? 'false' : 'true');
21
+ header.dataset.paneLoading = ready ? 'false' : 'true';
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');
24
+ for (var i = 0; i < actions.length; i++) {
25
+ var action = actions[i];
26
+ if (!ready) {
27
+ if (action.dataset.panePriorDisabled == null) action.dataset.panePriorDisabled = action.disabled ? 'true' : 'false';
28
+ action.disabled = true;
29
+ } else if (action.dataset.panePriorDisabled != null) {
30
+ action.disabled = action.dataset.panePriorDisabled === 'true';
31
+ delete action.dataset.panePriorDisabled;
32
+ }
33
+ }
34
+ var close = paneEl.querySelector('.split-pane-close');
35
+ if (close) {
36
+ var host = paneEl.parentNode;
37
+ var singletonOnly = !!host && host.id === 'split-host' && host.dataset && host.dataset.singlePane === 'true' &&
38
+ host.querySelectorAll('.split-pane').length === 1;
39
+ close.hidden = singletonOnly;
40
+ close.disabled = !ready || singletonOnly;
41
+ }
42
+ var title = paneEl.querySelector('.split-pane-title');
43
+ if (title) title.setAttribute('aria-disabled', ready ? 'false' : 'true');
44
+ return ready;
45
+ }