clay-server 5.2.1-beta.2 → 5.3.0-beta.2

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 (154) 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 +222 -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 -41
  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/session-folders.css +1 -1
  32. package/lib/public/css/split-view-layout.css +463 -0
  33. package/lib/public/css/split-view-worker-activity.css +97 -0
  34. package/lib/public/css/sticky-notes.css +13 -0
  35. package/lib/public/index.html +3 -3
  36. package/lib/public/modules/app-connection.js +4 -1
  37. package/lib/public/modules/app-favicon.js +1 -1
  38. package/lib/public/modules/app-messages.js +31 -9
  39. package/lib/public/modules/chat-render-runtime.js +54 -0
  40. package/lib/public/modules/clay-control-copy.js +3 -0
  41. package/lib/public/modules/clay-navigation-render.js +1 -1
  42. package/lib/public/modules/clay-owner-navigation.js +15 -5
  43. package/lib/public/modules/clay-split-view-screen.js +74 -0
  44. package/lib/public/modules/clay-tool-activity.js +5 -0
  45. package/lib/public/modules/command-palette.js +30 -28
  46. package/lib/public/modules/contextual-clay-session.js +26 -2
  47. package/lib/public/modules/contextual-clay.js +5 -2
  48. package/lib/public/modules/contextual-mate-sessions.js +2 -0
  49. package/lib/public/modules/filebrowser.js +6 -2
  50. package/lib/public/modules/generation-stop.js +7 -1
  51. package/lib/public/modules/knowledge-workbench-entry.js +43 -0
  52. package/lib/public/modules/markdown-view-mode.js +34 -0
  53. package/lib/public/modules/markdown.js +11 -0
  54. package/lib/public/modules/mate-knowledge-workbench.js +2 -7
  55. package/lib/public/modules/mate-sidebar.js +1 -1
  56. package/lib/public/modules/palette-clay-conversation.js +87 -0
  57. package/lib/public/modules/palette-render-helpers.js +25 -0
  58. package/lib/public/modules/pane-activation.js +46 -7
  59. package/lib/public/modules/pane-binding.js +12 -2
  60. package/lib/public/modules/pane-bridge.js +12 -3
  61. package/lib/public/modules/pane-deferred-bootstrap.js +32 -0
  62. package/lib/public/modules/pane-feature-assets.js +35 -0
  63. package/lib/public/modules/pane-file-bridge.js +8 -3
  64. package/lib/public/modules/pane-search-receipt.js +12 -0
  65. package/lib/public/modules/permission-control.js +6 -6
  66. package/lib/public/modules/popup-chat-rendering.js +244 -0
  67. package/lib/public/modules/project-activation.js +2 -2
  68. package/lib/public/modules/qrcode.js +12 -1
  69. package/lib/public/modules/right-workbench.js +68 -0
  70. package/lib/public/modules/search-hints.js +79 -0
  71. package/lib/public/modules/session-context-menu.js +35 -2
  72. package/lib/public/modules/session-folder-collapse.js +2 -1
  73. package/lib/public/modules/session-folder-context.js +24 -13
  74. package/lib/public/modules/session-folder-delete.js +18 -1
  75. package/lib/public/modules/session-folder-dnd.js +4 -1
  76. package/lib/public/modules/session-folder-layout.js +7 -6
  77. package/lib/public/modules/session-search-pane-state.js +30 -0
  78. package/lib/public/modules/session-search.js +53 -13
  79. package/lib/public/modules/sidebar-mobile.js +3 -3
  80. package/lib/public/modules/sidebar-projects.js +1 -0
  81. package/lib/public/modules/sidebar-session-folders-ui.js +3 -3
  82. package/lib/public/modules/sidebar-sessions.js +57 -24
  83. package/lib/public/modules/sidebar.js +20 -3
  84. package/lib/public/modules/split-group-helpers.js +1 -1
  85. package/lib/public/modules/split-group-restore.js +8 -11
  86. package/lib/public/modules/split-pair-ui.js +18 -0
  87. package/lib/public/modules/split-pane-header-state.js +45 -0
  88. package/lib/public/modules/split-pane-reconciler.js +23 -4
  89. package/lib/public/modules/split-pane-renderer.js +330 -23
  90. package/lib/public/modules/split-pane-retention.js +157 -0
  91. package/lib/public/modules/split-session-boundary.js +21 -0
  92. package/lib/public/modules/split-view-create-action.js +15 -0
  93. package/lib/public/modules/split-view-create-menu.js +83 -0
  94. package/lib/public/modules/split-view-create-request.js +23 -0
  95. package/lib/public/modules/split-view-layout-client.js +198 -0
  96. package/lib/public/modules/split-view-layout-icons.js +37 -0
  97. package/lib/public/modules/split-view-layout-scope.js +38 -0
  98. package/lib/public/modules/split-view-organization.js +73 -0
  99. package/lib/public/modules/split-view-presentation-close.js +35 -0
  100. package/lib/public/modules/split-view-presentation-drag.js +278 -0
  101. package/lib/public/modules/split-view-presentation-drop.js +57 -0
  102. package/lib/public/modules/split-view-presentation-lookup.js +44 -0
  103. package/lib/public/modules/split-view-presentation-renderer.js +494 -0
  104. package/lib/public/modules/split-view-presentations.js +498 -0
  105. package/lib/public/modules/split-view-shell-controls.js +28 -0
  106. package/lib/public/modules/split-view-sidebar-menu.js +42 -0
  107. package/lib/public/modules/split-view-sidebar-renderer.js +30 -0
  108. package/lib/public/modules/split-view-singleton.js +21 -0
  109. package/lib/public/modules/split-view-worker-activity.js +94 -0
  110. package/lib/public/modules/split-view-worker-controls.js +30 -0
  111. package/lib/public/modules/split-view-worker-removal.js +137 -0
  112. package/lib/public/modules/split-view-worker-scroll.js +57 -0
  113. package/lib/public/modules/split-view-worker-transcript.js +46 -0
  114. package/lib/public/modules/split-view-workers.js +418 -0
  115. package/lib/public/modules/split-view.js +61 -22
  116. package/lib/public/modules/sticky-notes.js +9 -2
  117. package/lib/public/modules/tools.js +25 -5
  118. package/lib/public/modules/workbench-layout-preference.js +76 -0
  119. package/lib/public/modules/worker-created-presentation.js +37 -0
  120. package/lib/public/pane-assets/app-2DRBYY3L.js +1 -0
  121. package/lib/public/pane-assets/chunk-4BM7UVNG.js +2 -0
  122. package/lib/public/pane-assets/chunk-6SM2WQJX.js +1 -0
  123. package/lib/public/pane-assets/chunk-7H36SUZA.js +2 -0
  124. package/lib/public/pane-assets/chunk-C4RVVYNT.js +1 -0
  125. package/lib/public/pane-assets/chunk-DGOFGEUK.js +1 -0
  126. package/lib/public/pane-assets/chunk-I57URYSL.js +1 -0
  127. package/lib/public/pane-assets/chunk-O6MQL6MR.js +167 -0
  128. package/lib/public/pane-assets/chunk-PO2JOHHW.js +1 -0
  129. package/lib/public/pane-assets/chunk-UMGHINOT.js +1 -0
  130. package/lib/public/pane-assets/manifest.json +553 -0
  131. package/lib/public/pane-assets/style-63EZCREE.css +1 -0
  132. package/lib/public/style.css +5 -1
  133. package/lib/server-owner-ui.js +6 -2
  134. package/lib/server-owner-view-observation.js +34 -0
  135. package/lib/server-palette.js +1 -0
  136. package/lib/server-pane-bootstrap.js +24 -0
  137. package/lib/server-settings.js +2 -0
  138. package/lib/server-static-assets.js +52 -0
  139. package/lib/server-workbench-layout-settings.js +56 -0
  140. package/lib/server.js +6 -45
  141. package/lib/session-activity-state.js +28 -0
  142. package/lib/session-list-delivery.js +17 -0
  143. package/lib/session-pair-turn-control.js +1 -6
  144. package/lib/session-search.js +3 -0
  145. package/lib/session-split-group-anchors.js +2 -2
  146. package/lib/session-split-group-roles.js +5 -8
  147. package/lib/session-split-group-v2-store.js +0 -3
  148. package/lib/sessions.js +16 -6
  149. package/lib/split-view-layout.js +132 -0
  150. package/lib/users-workbench-layout-preferences.js +33 -0
  151. package/lib/users.js +3 -0
  152. package/lib/workspace-query-service.js +1 -5
  153. package/lib/ws-schema.js +16 -0
  154. package/package.json +6 -3
@@ -7,12 +7,12 @@
7
7
  import { store } from './store.js';
8
8
  import { iconHtml, refreshIcons } from './icons.js';
9
9
  import { escapeHtml } from './utils.js';
10
+ import { closeSessionContextMenu, positionSessionContextMenu } from './session-context-menu.js';
10
11
 
11
12
  var LONG_PRESS_MS = 500;
12
13
  var MOVE_TOLERANCE = 10;
13
14
  var POST_RELEASE_MS = 400;
14
15
  var HOLD_MAX_MS = 15000;
15
- var EDGE = 8;
16
16
 
17
17
  function contextState() {
18
18
  return store.get('sessionFolderContext') || { press: null, held: null, suppressUntil: 0 };
@@ -72,7 +72,7 @@ function onMenuKey(event) {
72
72
 
73
73
  // Opens the menu with its top-left corner at the point, kept inside the viewport.
74
74
  export function openFolderMenuAt(anchor, entries, point) {
75
- closeFolderMenu();
75
+ closeFolderMenu(false); closeSessionContextMenu(false);
76
76
  var menu = document.createElement("div");
77
77
  menu.className = "session-ctx-menu session-folder-menu";
78
78
  menu.setAttribute("role", "menu");
@@ -94,16 +94,12 @@ export function openFolderMenuAt(anchor, entries, point) {
94
94
  });
95
95
  menu.style.visibility = "hidden";
96
96
  document.body.appendChild(menu);
97
- var box = menu.getBoundingClientRect();
98
- var left = Math.min(Math.max(EDGE, point.x), Math.max(EDGE, window.innerWidth - box.width - EDGE));
99
- var top = point.y;
100
- if (top + box.height > window.innerHeight - EDGE) top = Math.min(point.y - box.height, window.innerHeight - box.height - EDGE);
101
- top = Math.max(EDGE, top);
102
- menu.style.left = left + "px";
103
- menu.style.top = top + "px";
104
- menu.style.right = "auto";
97
+ positionSessionContextMenu(menu, anchor, point);
105
98
  menu.style.visibility = "";
106
- store.set({ sessionFolderMenu: { el: menu, anchor: anchor } });
99
+ var root = anchor.closest('.session-folder-folder');
100
+ store.set({ sessionFolderMenu: { el: menu, anchor: anchor, folderId: root && root.dataset.folderId,
101
+ mobile: !!(root && root.classList.contains('is-mobile')), slug: store.get('currentSlug'), viewerId: store.get('myUserId'),
102
+ folders: JSON.stringify((store.get('sessionFolders') || {}).folders || []) } });
107
103
  document.addEventListener("pointerdown", onMenuOutside, true);
108
104
  document.addEventListener("mousedown", onMenuOutside, true);
109
105
  document.addEventListener("touchstart", onMenuOutside, true);
@@ -114,6 +110,17 @@ export function openFolderMenuAt(anchor, entries, point) {
114
110
  if (first) first.focus({ preventScroll: true });
115
111
  }
116
112
 
113
+ export function reconcileFolderMenu() {
114
+ var menu = store.get('sessionFolderMenu'); if (!menu) return;
115
+ if (!menu.folderId || menu.slug !== store.get('currentSlug') || menu.viewerId !== store.get('myUserId') ||
116
+ menu.folders !== JSON.stringify((store.get('sessionFolders') || {}).folders || [])) { closeFolderMenu(false); return; }
117
+ var roots = document.querySelectorAll('.session-folder-folder');
118
+ var root = Array.from(roots).find(function (item) { return item.dataset.folderId === menu.folderId && item.classList.contains('is-mobile') === menu.mobile; });
119
+ var anchor = root && root.querySelector('.session-folder-toggle');
120
+ if (!anchor) { closeFolderMenu(false); return; }
121
+ if (anchor !== menu.anchor) store.set({ sessionFolderMenu: Object.assign({}, menu, { anchor: anchor }) });
122
+ }
123
+
117
124
  // --- Long press (touch and pen) ---
118
125
 
119
126
  function onScrollCancel() {
@@ -164,14 +171,14 @@ export function consumeFolderClickSuppress() {
164
171
  // entries when it opens, so they always reflect the current folder order.
165
172
  export function wireFolderContext(header, anchor, entriesFor) {
166
173
  function open(x, y) {
167
- if (store.get('sessionFolderMenu')) return;
168
174
  openFolderMenuAt(anchor, entriesFor(), { x: x, y: y });
169
175
  }
170
176
 
171
177
  header.addEventListener("contextmenu", function (event) {
172
178
  event.preventDefault();
173
179
  event.stopPropagation();
174
- open(event.clientX, event.clientY);
180
+ var rect = anchor.getBoundingClientRect();
181
+ open(event.clientX || event.clientY ? event.clientX : rect.left + 16, event.clientX || event.clientY ? event.clientY : rect.bottom);
175
182
  });
176
183
 
177
184
  header.addEventListener("keydown", function (event) {
@@ -213,3 +220,7 @@ export function wireFolderContext(header, anchor, entriesFor) {
213
220
  header.addEventListener("pointerleave", cancelFolderPress);
214
221
  header.addEventListener("dragstart", cancelFolderPress);
215
222
  }
223
+
224
+ store.subscribe(function (state, previous) {
225
+ if (state.myUserId !== previous.myUserId || previous.connected && !state.connected) closeFolderMenu(false);
226
+ });
@@ -178,13 +178,28 @@ function radioRow(mode, labelText, hint, current, extraClass) {
178
178
  return row;
179
179
  }
180
180
 
181
+ // Display context only. Session counts and deletion authority still come from the preview.
182
+ function folderHasSavedViews(folderId) {
183
+ var snapshot = store.snap();
184
+ if (!snapshot.connected || !snapshot.authIdentityLoaded || snapshot.dmMode || snapshot.paneMode ||
185
+ !snapshot.currentSlug || snapshot.activeProjectSlug !== snapshot.currentSlug || !snapshot.projectSplitViewHydrated || snapshot.projectSplitViewError ||
186
+ snapshot.projectSplitViewRequestSlug !== snapshot.currentSlug || snapshot.projectSplitViewRequestSocket !== getWs() ||
187
+ snapshot.projectSplitViewRequestOwnerId !== (snapshot.myUserId || null) ||
188
+ snapshot.projectSplitViewRequestGeneration !== snapshot.projectSplitViewSocketGeneration) return false;
189
+ return (snapshot.projectSplitViews || []).some(function (view) { return view.folderId === folderId; });
190
+ }
191
+
181
192
  function describe(current) {
182
193
  var preview = current.preview;
194
+ var hasViews = folderHasSavedViews(current.folderId);
195
+ var viewCopy = "Saved Split views will move to Unfiled and keep their Favorites status.";
196
+ if (preview.sessionCount === 0 && hasViews) return 'The folder "' + current.label + '" contains saved Split views. ' + viewCopy + " No sessions will be deleted.";
183
197
  if (preview.sessionCount === 0) return 'The folder "' + current.label + '" is empty. Deleting it does not affect any sessions.';
184
198
  var text = 'The folder "' + current.label + '" contains ' + phrase(preview.sessionCount, "session") + ".";
185
199
  if (preview.workerCount > 0) {
186
200
  text += " " + phrase(preview.workerCount, "Split Worker session") + " owned by those Drivers would be deleted with them, including hidden sessions and previous generations.";
187
201
  }
202
+ if (hasViews) text += " " + viewCopy + " The choices below apply to individually filed sessions; deleting member sessions also removes them from their Split views.";
188
203
  return text;
189
204
  }
190
205
 
@@ -327,7 +342,9 @@ export function handleFolderDeleteState(msg) {
327
342
  store.subscribe(function (next, previous) {
328
343
  if (next.sessionFolderDelete !== previous.sessionFolderDelete) paint();
329
344
  // Retry depends on the connection, so a change in it repaints the dialog.
330
- else if (next.connected !== previous.connected && next.sessionFolderDelete) paint();
345
+ else if (next.sessionFolderDelete && (next.connected !== previous.connected || next.projectSplitViews !== previous.projectSplitViews ||
346
+ next.projectSplitViewHydrated !== previous.projectSplitViewHydrated || next.projectSplitViewError !== previous.projectSplitViewError ||
347
+ next.currentSlug !== previous.currentSlug || next.myUserId !== previous.myUserId)) paint();
331
348
  var current = next.sessionFolderDelete;
332
349
  if (next.currentSlug !== previous.currentSlug || next.dmMode !== previous.dmMode) clearDustParticles("folder-delete-dust");
333
350
  if (!current || !previous.connected || next.connected) return;
@@ -186,8 +186,11 @@ function ghostFor(label) {
186
186
  }
187
187
 
188
188
  export function wireFolderDrag(header, folderId, label) {
189
- header.setAttribute("draggable", "true");
189
+ // Keep disclosure clicks separate from the native folder-reorder gesture.
190
+ var handle = header.querySelector('.session-folder-label') || header;
191
+ handle.setAttribute("draggable", "true");
190
192
  header.addEventListener("dragstart", function (event) {
193
+ if (event.target.closest && event.target.closest('.session-folder-chevron, .session-folder-new-btn')) { event.preventDefault(); return; }
191
194
  var root = header.closest(".session-folder-folder");
192
195
  var parent = root && root.parentElement;
193
196
  if (!parent) return;
@@ -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 () {