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
@@ -0,0 +1,244 @@
1
+ // Popup transcript presentation hooks. Tool identity, lifecycle and evidence
2
+ // stay owned by tools.js; this module only controls the compact activity view.
3
+ import { iconHtml } from './icons.js';
4
+ import { syncToolGroupDisclosure } from './clay-tool-activity.js';
5
+
6
+ // Mounted transcript nodes own completed records without retaining removed history.
7
+ var mountedToolGroups = new WeakMap();
8
+ export function rememberMountedToolGroup(group) { mountedToolGroups.set(group.el, group); }
9
+ export function mountedToolGroup(element) { return mountedToolGroups.get(element) || null; }
10
+ export function transcriptToolGroups(root) {
11
+ return root ? Array.from(root.querySelectorAll('.tool-group')).map(mountedToolGroup).filter(function (group) { return !!group; }) : [];
12
+ }
13
+
14
+ export function setPopupChatRendering(enabled) {
15
+ if (typeof document === 'undefined' || !document.body) return false;
16
+ document.body.classList.toggle('popup-chat-rendering', enabled === true);
17
+ if (!enabled) {
18
+ var rows = document.querySelectorAll('.popup-chat-activity-row[hidden]');
19
+ for (var i = 0; i < rows.length; i++) rows[i].hidden = false;
20
+ var groups = document.querySelectorAll('.popup-chat-activity');
21
+ for (var j = 0; j < groups.length; j++) {
22
+ var items = groups[j].querySelector('.tool-group-items');
23
+ if (items) {
24
+ var expanded = !groups[j].classList.contains('collapsed');
25
+ items.inert = !expanded;
26
+ items.setAttribute('aria-hidden', String(!expanded));
27
+ }
28
+ }
29
+ }
30
+ return enabled === true;
31
+ }
32
+
33
+ export function shouldEnablePopupChatRendering(body, state, frame, identity) {
34
+ return !!(body && body.classList && body.classList.contains('contextual-chat-pane') && state && state.paneMode && frame &&
35
+ identity && identity.kind === 'mate' && identity.mateId && identity.mateId === frame.dataset.mateId &&
36
+ state.myUserId && state.sessionOriginId && state.paneSessionId && state.activeSessionId === state.paneSessionId &&
37
+ (!frame.dataset.expectedSessionOrigin || frame.dataset.expectedSessionOrigin === state.sessionOriginId));
38
+ }
39
+
40
+ export function popupChatContextIsReady(body, state, frame, identity, surface) {
41
+ if (!body || !body.classList || !body.classList.contains('contextual-chat-pane')) return false;
42
+ if (!state || !state.paneMode || !state.connected || !state.myUserId || !state.contextualSessionReady) return false;
43
+ if (!state.sessionOriginId || state.activeSessionId !== state.paneSessionId) return false;
44
+ if (!frame || !frame.dataset || !frame.dataset.mateId || frame.dataset.ownerId !== state.myUserId) return false;
45
+ if (frame.hidden || frame.inert || !surface || surface.dataset.mode !== 'popover' || !surface.contains(frame)) return false;
46
+ if (String(frame.dataset.sessionId || '') !== String(state.paneSessionId || '')) return false;
47
+ if (frame.dataset.expectedSessionOrigin && frame.dataset.expectedSessionOrigin !== state.sessionOriginId) return false;
48
+ if (state.activeProjectMateId !== frame.dataset.mateId) return false;
49
+ if (!identity || identity.kind !== 'mate' || identity.mateId !== frame.dataset.mateId) return false;
50
+ return true;
51
+ }
52
+
53
+ export function popupChatRenderingEnabled(identity) {
54
+ return !!(identity && identity.kind === 'mate' && typeof document !== 'undefined' &&
55
+ document.body && document.body.classList.contains('popup-chat-rendering'));
56
+ }
57
+
58
+ export function popupChatRenderingActive() {
59
+ return typeof document !== 'undefined' && document.body && document.body.classList.contains('popup-chat-rendering');
60
+ }
61
+
62
+ export function reconcilePopupToolGroups(groups, active, activate, deactivate) {
63
+ for (var i = 0; i < groups.length; i++) {
64
+ var group = groups[i];
65
+ if (!group) continue;
66
+ if (active) {
67
+ group.popupChat = true;
68
+ activate(group);
69
+ } else if (group.popupChat) {
70
+ deactivate(group);
71
+ group.popupChat = false;
72
+ }
73
+ }
74
+ }
75
+
76
+ function decorateTool(tool) {
77
+ if (!tool || !tool.el || tool.popupOriginalName !== undefined) return;
78
+ var name = tool.el.querySelector('.tool-name');
79
+ tool.popupOriginalName = name.textContent;
80
+ tool.popupOriginalHidden = tool.el.hidden;
81
+ tool.popupOriginalInert = tool.el.inert;
82
+ tool.el.classList.add('popup-chat-activity-row');
83
+ // Native descriptions own reference nodes and their handlers throughout mode changes.
84
+ if (!tool.clayActivity) {
85
+ tool.popupPresentedName = popupToolActionLabel(tool.name);
86
+ name.textContent = tool.popupPresentedName;
87
+ }
88
+ }
89
+
90
+ function decorateGroup(group) {
91
+ if (!group || !group.el || group.popupOriginalState) return;
92
+ var header = group.el.querySelector('.tool-group-header');
93
+ var label = group.el.querySelector('.tool-group-label');
94
+ group.popupOriginalState = {
95
+ label: label.textContent,
96
+ collapsed: group.el.classList.contains('collapsed'),
97
+ userToggled: !!group.userToggled,
98
+ groupDisplay: group.el.style.display,
99
+ headerDisplay: header.style.display,
100
+ headerAriaExpanded: header.getAttribute('aria-expanded'),
101
+ headerAriaLabel: header.getAttribute('aria-label'),
102
+ headerTitle: header.title
103
+ };
104
+ group.el.classList.add('popup-chat-activity');
105
+ var icon = document.createElement('span');
106
+ icon.className = 'popup-chat-activity-icon';
107
+ icon.setAttribute('aria-hidden', 'true');
108
+ icon.innerHTML = iconHtml('activity');
109
+ var heading = document.createElement('span');
110
+ heading.className = 'popup-chat-activity-heading';
111
+ heading.textContent = 'Activity';
112
+ label.textContent = '';
113
+ label.appendChild(heading);
114
+ var count = document.createElement('span');
115
+ count.className = 'popup-chat-activity-count';
116
+ count.setAttribute('aria-label', '0 recorded events');
117
+ header.insertBefore(icon, label);
118
+ header.insertBefore(count, header.querySelector('.tool-group-status-icon'));
119
+ for (var i = 0; i < group.toolIds.length; i++) decorateTool(group.toolRecords[group.toolIds[i]]);
120
+ }
121
+
122
+ function restoreGroup(group) {
123
+ if (!group || !group.el) return;
124
+ for (var i = 0; i < group.toolIds.length; i++) {
125
+ var tool = group.toolRecords[group.toolIds[i]];
126
+ if (!tool || !tool.el || tool.popupOriginalName === undefined) continue;
127
+ var name = tool.el.querySelector('.tool-name');
128
+ if (!tool.clayActivity && name.textContent === tool.popupPresentedName) name.textContent = tool.popupOriginalName;
129
+ tool.el.hidden = tool.popupOriginalHidden;
130
+ tool.el.inert = tool.popupOriginalInert;
131
+ tool.el.classList.remove('popup-chat-activity-row');
132
+ delete tool.popupOriginalName;
133
+ delete tool.popupPresentedName;
134
+ delete tool.popupOriginalHidden;
135
+ delete tool.popupOriginalInert;
136
+ }
137
+ var original = group.popupOriginalState;
138
+ group.popupOriginalState = null;
139
+ if (!original) return;
140
+ var label = group.el.querySelector('.tool-group-label');
141
+ var header = group.el.querySelector('.tool-group-header');
142
+ label.textContent = original.label;
143
+ group.el.style.display = original.groupDisplay;
144
+ header.style.display = original.headerDisplay;
145
+ group.userToggled = original.userToggled;
146
+ group.el.classList.toggle('collapsed', original.collapsed);
147
+ if (original.headerAriaLabel === null) header.removeAttribute('aria-label');
148
+ else header.setAttribute('aria-label', original.headerAriaLabel);
149
+ header.title = original.headerTitle;
150
+ var icon = header.querySelector('.popup-chat-activity-icon');
151
+ var count = header.querySelector('.popup-chat-activity-count');
152
+ if (icon) icon.remove();
153
+ if (count) count.remove();
154
+ group.el.classList.remove('popup-chat-activity');
155
+ syncToolGroupDisclosure(group.el);
156
+ if (original.headerAriaExpanded !== null) header.setAttribute('aria-expanded', original.headerAriaExpanded);
157
+ }
158
+
159
+ export function syncPopupChatToolGroups(enabled, groups, identity) {
160
+ var active = !!(enabled && popupChatRenderingActive() && identity && identity.kind === 'mate');
161
+ reconcilePopupToolGroups(groups, active, function (group) {
162
+ decorateGroup(group);
163
+ for (var i = 0; i < group.toolIds.length; i++) decorateTool(group.toolRecords[group.toolIds[i]]);
164
+ group.el.style.display = '';
165
+ group.el.querySelector('.tool-group-header').style.display = '';
166
+ if (!group.userToggled) group.el.classList.add('collapsed');
167
+ syncToolGroupDisclosure(group.el);
168
+ syncPopupActivityGroup(group);
169
+ }, restoreGroup);
170
+ }
171
+
172
+ function hasIndependentControl(row) {
173
+ if (!row || !row.querySelectorAll) return false;
174
+ var controls = row.querySelectorAll('button, a[href], input, select, textarea, [role="button"], [role="menuitem"], [role="option"]');
175
+ var disclosure = row.querySelector('.tool-header');
176
+ if (row.querySelector('.tool-desc-link')) return true;
177
+ for (var i = 0; i < controls.length; i++) {
178
+ if (controls[i] !== disclosure) return true;
179
+ }
180
+ return false;
181
+ }
182
+
183
+ export function syncPopupActivityGroup(group) {
184
+ if (!group || !group.popupChat || !group.el) return false;
185
+ var header = group.el.querySelector('.tool-group-header');
186
+ var items = group.el.querySelector('.tool-group-items');
187
+ var count = group.el.querySelector('.popup-chat-activity-count');
188
+ if (!header || !items) return false;
189
+
190
+ var expanded = !group.el.classList.contains('collapsed');
191
+ var rows = items.children;
192
+ var firstVisible = Math.max(0, rows.length - 2);
193
+ var pinned = 0;
194
+ for (var i = 0; i < rows.length; i++) {
195
+ var row = rows[i];
196
+ var keepVisible = hasIndependentControl(row);
197
+ if (keepVisible) pinned++;
198
+ row.classList.add('popup-chat-activity-row');
199
+ row.hidden = !expanded && i < firstVisible && !keepVisible;
200
+ row.inert = row.hidden;
201
+ }
202
+
203
+ // The native group disclosure remains the one control for revealing older
204
+ // activity. In popup mode the visible recent rows stay reachable even while
205
+ // the rest of the group is collapsed.
206
+ items.inert = false;
207
+ items.setAttribute('aria-hidden', 'false');
208
+ header.setAttribute('aria-expanded', String(expanded));
209
+ header.setAttribute('aria-label', 'Activity, ' + group.toolCount + ' recorded events. ' + (expanded ? 'Hide older activity' : 'Show older activity'));
210
+ header.title = expanded ? 'Hide older activity' : 'Show older activity';
211
+ if (count) {
212
+ count.textContent = String(group.toolCount);
213
+ count.setAttribute('aria-label', group.toolCount + ' recorded events');
214
+ count.dataset.pinnedCount = String(pinned);
215
+ }
216
+ return true;
217
+ }
218
+
219
+ export function popupToolActionLabel(name) {
220
+ var labels = {
221
+ Read: 'Read', Edit: 'Edit', Write: 'Write', Bash: 'Command',
222
+ Grep: 'Search files', Glob: 'Find files', WebSearch: 'Web search',
223
+ WebFetch: 'Fetch page', Task: 'Task'
224
+ };
225
+ return labels[name] || name || 'Tool activity';
226
+ }
227
+
228
+ export function popupToolDescription(name, input) {
229
+ input = input && typeof input === 'object' ? input : {};
230
+ if (name === 'Read' || name === 'Edit' || name === 'Write') {
231
+ var filePath = typeof input.file_path === 'string' ? input.file_path : '';
232
+ var parts = filePath.split('/');
233
+ return parts.length > 3 ? '…/' + parts.slice(-3).join('/') : filePath;
234
+ }
235
+ if (name === 'Bash') return typeof input.description === 'string' ? input.description.slice(0, 100) : '';
236
+ if (name === 'Grep') return typeof input.pattern === 'string' ? input.pattern.slice(0, 100) : '';
237
+ if (name === 'Glob') return typeof input.pattern === 'string' ? input.pattern.slice(0, 100) : '';
238
+ if (name === 'WebSearch') return typeof input.query === 'string' ? input.query.slice(0, 100) : '';
239
+ if (name === 'WebFetch') {
240
+ try { return new URL(input.url).hostname; } catch (error) { return ''; }
241
+ }
242
+ if (name === 'Task') return typeof input.description === 'string' ? input.description.slice(0, 100) : '';
243
+ return '';
244
+ }
@@ -70,7 +70,7 @@ export function isProjectSessionReady(state, socket) {
70
70
  }
71
71
 
72
72
  export function splitPanesMatchProject(split, slug) {
73
- if (!split || !Array.isArray(split.panes) || split.panes.length < 2 || split.panes.length > 3 || !slug) return false;
73
+ if (!split || !Array.isArray(split.panes) || split.panes.length < 2 || !slug) return false;
74
74
  for (var i = 0; i < split.panes.length; i++) if (split.panes[i].slug !== slug) return false;
75
75
  return true;
76
76
  }
@@ -78,7 +78,7 @@ export function splitPanesMatchProject(split, slug) {
78
78
  export function hasCurrentSplitGroup(split, groups) {
79
79
  if (!split || !split.groupId) return true;
80
80
  var members = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
81
- if (!members || members.length < 2 || members.length > 3) return false;
81
+ if (!members || members.length < 2) return false;
82
82
  var available = Array.isArray(groups) ? groups : [];
83
83
  for (var i = 0; i < available.length; i++) {
84
84
  var group = available[i];
@@ -1,4 +1,6 @@
1
- import { copyToClipboard } from './utils.js';
1
+ import { ensurePaneFeatureAssets } from './pane-feature-assets.js';
2
+ import { store } from './store.js';
3
+ import { showToast, copyToClipboard } from './utils.js';
2
4
 
3
5
  var activeShareUrl = null;
4
6
  var activeShareTitle = null;
@@ -32,6 +34,15 @@ export function triggerShare(options) {
32
34
  return;
33
35
  }
34
36
 
37
+ if (typeof qrcode === 'undefined') {
38
+ var owner = store.get('myUserId'), slug = store.get('currentSlug'), generation = store.get('projectSplitViewSocketGeneration');
39
+ ensurePaneFeatureAssets('qr').then(function () {
40
+ if (owner === store.get('myUserId') && slug === store.get('currentSlug') && generation === store.get('projectSplitViewSocketGeneration')) triggerShare(options);
41
+ }).catch(function (error) {
42
+ if (owner === store.get('myUserId') && slug === store.get('currentSlug') && generation === store.get('projectSplitViewSocketGeneration')) showToast(error.message, 'error');
43
+ });
44
+ return;
45
+ }
35
46
  // Show QR overlay
36
47
  var qrOverlay = document.getElementById("qr-overlay");
37
48
  var qrCanvas = document.getElementById("qr-canvas");
@@ -1,8 +1,73 @@
1
1
  // Single arbitration point for the mutually exclusive right-side workbench.
2
2
 
3
3
  import { store } from './store.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { showToast } from './utils.js';
4
6
 
5
7
  var closers = {};
8
+ var panelIds = { terminal: 'terminal-container', browser: 'shared-browser-panel', 'file-viewer': 'file-viewer',
9
+ 'project-logs': 'project-logs-panel', you: 'you-panel', issues: 'issues-panel', linear: 'linear-panel',
10
+ 'notes-browser': 'notes-browser', 'scheduled-tasks': 'scheduled-tasks-panel', debates: 'debates-workbench',
11
+ 'mcp-skills': 'mcp-skills-workbench', 'mate-knowledge': 'mate-knowledge-workbench', wireframe: 'wireframe-panel' };
12
+
13
+ // Pane startup does not need account settings for a closed workbench.
14
+ function ensureLayoutPreference() {
15
+ var account = store.get('myUserId'), project = store.get('currentSlug');
16
+ return import('./workbench-layout-preference.js').then(function (api) {
17
+ if (account !== store.get('myUserId') || project !== store.get('currentSlug')) return false;
18
+ return api.ensureWorkbenchLayoutPreference();
19
+ }).catch(function () {
20
+ if (account !== store.get('myUserId') || project !== store.get('currentSlug')) return false;
21
+ store.set({ workbenchLayoutPreference: Object.assign({}, store.get('workbenchLayoutPreference') || {}, {
22
+ loading: false, saving: false, error: 'Workbench layout could not be loaded. Try again.'
23
+ }) });
24
+ return false;
25
+ });
26
+ }
27
+
28
+ function renderLayoutControls() {
29
+ if (typeof document === 'undefined') return;
30
+ var main = document.getElementById('main-panels');
31
+ if (!main) return;
32
+ var state = store.get('workbenchLayoutPreference') || {}, docked = state.mode === 'docked';
33
+ main.dataset.workbenchMode = docked ? 'docked' : 'overlay';
34
+ var panel = document.getElementById(panelIds[store.get('rightWorkbenchOwner')]);
35
+ if (!panel) return;
36
+ var header = panel.querySelector(':scope > header, :scope > .file-viewer-header .file-viewer-tabbar, :scope > .terminal-header');
37
+ if (!header) return;
38
+ var button = panel.querySelector('.workbench-layout-toggle');
39
+ if (!button) {
40
+ var actions = header.querySelector('[class*="window-actions"], .terminal-header-actions, .shared-browser-window, :scope.mcp-skills-header > div') || header;
41
+ button = document.createElement('button'); button.type = 'button'; button.className = 'workbench-layout-toggle';
42
+ button.addEventListener('click', function () {
43
+ var current = store.get('workbenchLayoutPreference') || {};
44
+ if (!current.ready || current.loading || current.saving) { ensureLayoutPreference(); return; }
45
+ var owner = store.get('myUserId'), project = store.get('currentSlug');
46
+ function currentTarget() { return button.isConnected && owner === store.get('myUserId') && project === store.get('currentSlug'); }
47
+ import('./workbench-layout-preference.js').then(function (api) {
48
+ var fresh = store.get('workbenchLayoutPreference') || {};
49
+ if (!currentTarget() || fresh.mode !== current.mode || fresh.revision !== current.revision || fresh.loading || fresh.saving || !fresh.ready) return false;
50
+ return api.saveWorkbenchLayoutMode(current.mode === 'docked' ? 'overlay' : 'docked');
51
+ }).then(function (saved) {
52
+ if (saved === false && currentTarget()) showToast((store.get('workbenchLayoutPreference') || {}).error || 'Workbench layout could not be saved. Try again.', 'error');
53
+ }).catch(function (error) {
54
+ if (currentTarget()) showToast(error.message || 'Workbench layout could not be saved. Try again.', 'error');
55
+ });
56
+ });
57
+ var anchor = actions === header ? header.querySelector('[data-knowledge-wide]') : actions.firstElementChild;
58
+ actions.insertBefore(button, anchor || null);
59
+ }
60
+ if (button.dataset.mode !== (docked ? 'docked' : 'overlay')) {
61
+ button.dataset.mode = docked ? 'docked' : 'overlay';
62
+ button.innerHTML = iconHtml(docked ? 'layers-2' : 'panel-right');
63
+ refreshIcons();
64
+ }
65
+ button.setAttribute('aria-label', docked ? 'Float workbench over sessions' : 'Dock workbench beside sessions');
66
+ button.title = state.error || button.getAttribute('aria-label');
67
+ button.setAttribute('aria-pressed', docked ? 'true' : 'false');
68
+ button.disabled = !!state.loading || !!state.saving;
69
+ button.setAttribute('aria-busy', state.loading || state.saving ? 'true' : 'false');
70
+ }
6
71
 
7
72
  export function registerRightWorkbench(name, close) {
8
73
  if (!name || typeof close !== "function") return;
@@ -10,6 +75,7 @@ export function registerRightWorkbench(name, close) {
10
75
  }
11
76
 
12
77
  export function claimRightWorkbench(name) {
78
+ if (typeof document !== 'undefined') ensureLayoutPreference();
13
79
  var previousRevision = store.get("rightWorkbenchRevision") || 0;
14
80
  var nextRevision = previousRevision + 1;
15
81
  storeSet(name, nextRevision);
@@ -18,6 +84,7 @@ export function claimRightWorkbench(name) {
18
84
  if (keys[i] === name) continue;
19
85
  try { closers[keys[i]](); } catch (error) { /* one panel must not block another */ }
20
86
  }
87
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(renderLayoutControls);
21
88
  return nextRevision;
22
89
  }
23
90
 
@@ -39,6 +106,7 @@ export function resetRightWorkbench() {
39
106
  }
40
107
 
41
108
  store.subscribe(function (state, previous) {
109
+ if (state.workbenchLayoutPreference !== previous.workbenchLayoutPreference || state.rightWorkbenchOwner !== previous.rightWorkbenchOwner) renderLayoutControls();
42
110
  if (state.currentSlug === previous.currentSlug && state.myUserId === previous.myUserId) return;
43
111
  resetRightWorkbench();
44
112
  });
@@ -0,0 +1,79 @@
1
+ import { store } from './store.js';
2
+
3
+ export var SEARCH_HINT_INTERVAL = 180000;
4
+ export var SEARCH_HINT_QUESTIONS = Object.freeze([
5
+ 'What was I working on recently?',
6
+ 'Where did I leave off yesterday?',
7
+ 'What did I review recently?',
8
+ "Find the session I said I'd return to.",
9
+ 'What work did I leave unfinished?',
10
+ 'What did we decide about this project?',
11
+ 'Why did we choose this approach?',
12
+ 'Find the conversation about this bug.',
13
+ 'Which sessions need my attention?',
14
+ "What's happening across this view?",
15
+ 'What are my Workers doing?',
16
+ 'Which sessions are waiting for approval?'
17
+ ]);
18
+ export function searchHintText(index) {
19
+ return 'Search or ask Clay, like “' + SEARCH_HINT_QUESTIONS[index % SEARCH_HINT_QUESTIONS.length] + '”';
20
+ }
21
+
22
+ var trigger = null, input = null, timer = null, unsubscribe = null;
23
+ function relevantFocus(target) { return !!target && (target === input || trigger && trigger.contains(target)); }
24
+ function available() {
25
+ return trigger && input && !document.hidden && !store.get('paneMode') && window === window.top &&
26
+ !store.get('searchHintsPaletteOpen') && !store.get('searchHintsHovered') && !store.get('searchHintsFocused');
27
+ }
28
+ function cancelTimer() { if (timer !== null) { clearTimeout(timer); timer = null; } }
29
+ function paint() {
30
+ var label = trigger.querySelector('.cmd-palette-searchbar-text');
31
+ var text = searchHintText(store.get('searchHintIndex') || 0);
32
+ if (label) label.textContent = text;
33
+ input.placeholder = text;
34
+ }
35
+ function schedule() {
36
+ if (!available()) { cancelTimer(); return; }
37
+ if (timer !== null) return;
38
+ timer = setTimeout(function () {
39
+ timer = null;
40
+ if (!available()) return;
41
+ store.set({ searchHintIndex: ((store.get('searchHintIndex') || 0) + 1) % SEARCH_HINT_QUESTIONS.length });
42
+ paint(); schedule();
43
+ }, SEARCH_HINT_INTERVAL);
44
+ }
45
+ function visibilityChanged() { cancelTimer(); schedule(); }
46
+ function enter() { store.set({ searchHintsHovered: true }); }
47
+ function leave() { store.set({ searchHintsHovered: false }); }
48
+ function focusIn(event) { store.set({ searchHintsFocused: relevantFocus(event.target) }); }
49
+ function focusOut(event) { store.set({ searchHintsFocused: relevantFocus(event.relatedTarget) }); }
50
+ export function setSearchHintsPaletteOpen(open) { store.set({ searchHintsPaletteOpen: open === true }); }
51
+ export function disposeSearchHints() {
52
+ cancelTimer();
53
+ if (!trigger) return;
54
+ trigger.removeEventListener('mouseenter', enter); trigger.removeEventListener('mouseleave', leave);
55
+ document.removeEventListener('focusin', focusIn); document.removeEventListener('focusout', focusOut);
56
+ document.removeEventListener('visibilitychange', visibilityChanged);
57
+ if (unsubscribe) unsubscribe();
58
+ trigger = null; input = null; unsubscribe = null;
59
+ }
60
+ export function initSearchHints(button, field) {
61
+ if (button) button.removeAttribute('title');
62
+ if (trigger === button && input === field) return;
63
+ disposeSearchHints();
64
+ if (!button || !field) return;
65
+ trigger = button; input = field;
66
+ store.set({ searchHintIndex: store.get('searchHintIndex') || 0, searchHintsPaletteOpen: false,
67
+ searchHintsHovered: trigger.matches(':hover'), searchHintsFocused: relevantFocus(document.activeElement) });
68
+ if (!store.get('searchHintsFocused')) paint();
69
+ trigger.addEventListener('mouseenter', enter); trigger.addEventListener('mouseleave', leave);
70
+ document.addEventListener('focusin', focusIn); document.addEventListener('focusout', focusOut);
71
+ document.addEventListener('visibilitychange', visibilityChanged);
72
+ unsubscribe = store.subscribe(function (state, previous) {
73
+ if (state.paneMode !== previous.paneMode || state.searchHintsPaletteOpen !== previous.searchHintsPaletteOpen ||
74
+ state.searchHintsHovered !== previous.searchHintsHovered || state.searchHintsFocused !== previous.searchHintsFocused) {
75
+ cancelTimer(); schedule();
76
+ }
77
+ });
78
+ schedule();
79
+ }
@@ -22,6 +22,7 @@ export function closeSessionContextMenu(restoreFocus) {
22
22
  document.removeEventListener("keydown", onMenuKey, true);
23
23
  window.removeEventListener("blur", onWindowBlur);
24
24
  if (state.anchor) state.anchor.setAttribute("aria-expanded", "false");
25
+ state.el.hidden = true;
25
26
  if (state.el && state.el.isConnected) state.el.remove();
26
27
  if (restoreFocus !== false && state.anchor && state.anchor.isConnected) {
27
28
  state.anchor.focus({ preventScroll: true });
@@ -54,10 +55,40 @@ function onMenuKey(event) {
54
55
  event.preventDefault();
55
56
  event.stopPropagation();
56
57
  closeSessionContextMenu(true);
58
+ return;
59
+ }
60
+ var items = Array.from(state.el.querySelectorAll('button:not([disabled])')), index = items.indexOf(document.activeElement), next = null;
61
+ if (!items.length) return;
62
+ if (event.key === 'ArrowDown' || event.key === 'Tab') next = (index + (event.shiftKey ? -1 : 1) + items.length) % items.length;
63
+ else if (event.key === 'ArrowUp') next = (index - 1 + items.length) % items.length;
64
+ else if (event.key === 'Home') next = 0;
65
+ else if (event.key === 'End') next = items.length - 1;
66
+ if (next !== null) { event.preventDefault(); event.stopPropagation(); items[next].focus({ preventScroll: true }); }
67
+ }
68
+
69
+ export function positionSessionContextMenu(menu, anchor, point) {
70
+ var rect = anchor.getBoundingClientRect(), box = menu.getBoundingClientRect();
71
+ var x = point ? point.x : rect.left, y = point ? point.y : rect.bottom + 2;
72
+ menu.style.left = Math.max(8, Math.min(x, window.innerWidth - box.width - 8)) + 'px';
73
+ menu.style.top = Math.max(8, Math.min(y, window.innerHeight - box.height - 8)) + 'px';
74
+ menu.style.right = 'auto';
75
+ }
76
+
77
+ export function reconcileSessionContextMenu() {
78
+ var state = menuState(); if (!state) return;
79
+ var anchor = state.resolveAnchor ? state.resolveAnchor() : state.anchor;
80
+ if (!anchor || !anchor.isConnected || state.slug !== store.get('currentSlug') || state.viewerId !== store.get('myUserId') || state.ws !== getWs()) {
81
+ closeSessionContextMenu(false); return;
57
82
  }
83
+ if (anchor === state.anchor) return;
84
+ state.anchor.setAttribute('aria-expanded', 'false');
85
+ var hadTabIndex = anchor.hasAttribute('tabindex'), tabIndex = anchor.getAttribute('tabindex');
86
+ anchor.setAttribute('aria-haspopup', 'menu'); anchor.setAttribute('aria-expanded', 'true');
87
+ if (!hadTabIndex) anchor.setAttribute('tabindex', '-1');
88
+ store.set({ sessionContextMenu: Object.assign({}, state, { anchor: anchor, anchorHadTabIndex: hadTabIndex, anchorTabIndex: tabIndex }) });
58
89
  }
59
90
 
60
- export function mountSessionContextMenu(menu, anchor, sessionId) {
91
+ export function mountSessionContextMenu(menu, anchor, sessionId, options) {
61
92
  closeSessionContextMenu(false);
62
93
  menu.setAttribute("role", "menu");
63
94
  var items = menu.querySelectorAll("button");
@@ -76,6 +107,8 @@ export function mountSessionContextMenu(menu, anchor, sessionId) {
76
107
  anchorHadTabIndex: anchorHadTabIndex,
77
108
  anchorTabIndex: anchorTabIndex,
78
109
  sessionId: sessionId || null,
110
+ resolveAnchor: options && options.resolveAnchor || null,
111
+ slug: store.get('currentSlug'), viewerId: store.get('myUserId'), ws: getWs(),
79
112
  } });
80
113
  document.addEventListener("pointerdown", onOutsidePress, true);
81
114
  document.addEventListener("mousedown", onOutsidePress, true);
@@ -205,7 +238,7 @@ export function startHeaderSessionInlineRename(title, control, session) {
205
238
  }
206
239
 
207
240
  store.subscribe(function (state, previous) {
208
- if (state.currentSlug !== previous.currentSlug || state.dmMode !== previous.dmMode ||
241
+ if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.dmMode !== previous.dmMode ||
209
242
  state.sessionActionEpoch !== previous.sessionActionEpoch || state.panePendingBinding !== previous.panePendingBinding ||
210
243
  (previous.connected && !state.connected)) {
211
244
  closeSessionContextMenu(false);
@@ -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;