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
@@ -1,3 +1,5 @@
1
+ import { showCreatedWorkerPresentation } from './worker-created-presentation.js';
2
+ import { handleWorkerRemovalResult } from './split-view-worker-removal.js';
1
3
  import { handleStudioDocumentMessage } from './clay-studio-document.js';
2
4
  import { saveSessionRichDraft, restoreSessionRichDraft } from './session-rich-drafts.js';
3
5
  import { refreshPaneActivation, primePaneSession } from './pane-activation.js';
@@ -48,11 +50,13 @@ import { handleDefaultVendorMessage } from './default-vendor.js';
48
50
  import { handleContextViewMessage } from './context-view-preference.js';
49
51
  import { handleHomeMateMemoryState, handleHomeMateKnowledgeState, syncHomeMateSettingsTarget, handleHomeMateAvatarError } from './home-mate-settings.js';
50
52
  import { renderKnowledgeList, handleKnowledgeContent, handleKnowledgeMutation } from './mate-knowledge.js';
51
- import { handleKnowledgeWorkbenchMessage } from './mate-knowledge-workbench.js';
53
+ import { handleKnowledgeWorkbenchMessage } from './knowledge-workbench-entry.js';
52
54
  import { renderMemoryList } from './mate-memory.js';
53
55
  import { handlePaletteSessionSwitch, handlePaletteHistoryDone, handlePaletteHistoryPrepended, setPaletteVersion } from './command-palette.js';
54
56
  import { handleFindInSessionResults } from './session-search.js';
55
57
  import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
58
+ import { handleProjectSplitViews, handleProjectSplitViewResult, requestProjectSplitViews } from './split-view-presentations.js';
59
+ import { handleSplitWorkerConversation, handleWorkerSessionActivity, handleWorkerConversationChanged } from './split-view-workers.js';
56
60
  import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
57
61
  import { handleSessionActionMessage } from './session-actions.js';
58
62
  import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
@@ -95,7 +99,7 @@ import { clearPairResultStatus, handlePairResultStatus } from './pair-result-sta
95
99
  import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, flushStreamBuffer, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
96
100
  import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
97
101
  import { setStatus } from './app-connection.js';
98
- import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
102
+ import { resolveSwitchedVendor } from './pane-session.js';
99
103
  import { markProjectSessionListHydrated, applyProjectSplitGroups, clearProjectSplitState } from './split-session-boundary.js';
100
104
  import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
101
105
  import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
@@ -440,6 +444,7 @@ export function processMessage(msg) {
440
444
  if (msg.projectOwnerId !== undefined) store.set({ currentProjectOwnerId: msg.projectOwnerId });
441
445
  if (msg.ownerLocked !== undefined) store.set({ ownerLocked: !!msg.ownerLocked });
442
446
  if (msg.osUsers !== undefined) store.set({ isOsUsers: !!msg.osUsers });
447
+ store.set({ paneOsIdentity: msg.paneOsIdentity || null });
443
448
  if (msg.lanHost) window.__lanHost = msg.lanHost;
444
449
  store.set({ skipPermsEnabled: msg.dangerouslySkipPermissions === true });
445
450
  var spBanner = document.getElementById("skip-perms-pill");
@@ -785,19 +790,31 @@ export function processMessage(msg) {
785
790
  renderMateSessionList(msg.sessions || []);
786
791
  renderSessionList(msg.sessions || []);
787
792
  store.set({ paneBindingSessions: msg.sessions || [] });
793
+ requestProjectSplitViews();
788
794
  refreshPaneActivation();
789
795
  syncGithubSessions(msg.sessions || []);
790
796
  syncPaneTitles();
791
- var panePin = resolvePaneSession(store.get('paneMode') && !primePaneSession(), store.get('panePinPending'), store.get('paneSessionId'), msg.sessions);
792
- if (panePin.consumed) {
793
- store.set({ panePinPending: false });
794
- if (panePin.sessionId && getWs() && getWs().readyState === 1) {
795
- getWs().send(JSON.stringify({ type: "switch_session", id: panePin.sessionId }));
796
- }
797
+ if (store.get('paneMode')) {
798
+ // A pane pin must use the authenticated activation binding. Sending
799
+ // this startup URL's session as a plain switch can race a parent
800
+ // retarget and restore the iframe's stale initial session.
801
+ primePaneSession();
797
802
  }
798
803
  handlePaletteSessionSwitch();
799
804
  maybeRestoreSplitGroup();
800
805
  break;
806
+ case "project_split_views":
807
+ handleProjectSplitViews(msg);
808
+ break;
809
+ case "project_split_view_result":
810
+ handleProjectSplitViewResult(msg);
811
+ break;
812
+ case "split_worker_conversation_changed":
813
+ handleWorkerConversationChanged(msg);
814
+ break;
815
+ case "split_worker_conversation":
816
+ handleSplitWorkerConversation(msg);
817
+ break;
801
818
  case "session_github_status":
802
819
  applyGithubPersonalReviewStatus(msg);
803
820
  break;
@@ -813,6 +830,10 @@ export function processMessage(msg) {
813
830
  if (!msg.ok && msg.error) showToast(msg.error, "error");
814
831
  break;
815
832
 
833
+ case "split_worker_remove_result":
834
+ handleWorkerRemovalResult(msg);
835
+ break;
836
+
816
837
  case "split_worker_close_result":
817
838
  if (!msg.ok && msg.error) showToast(msg.error, "error");
818
839
  break;
@@ -823,7 +844,7 @@ export function processMessage(msg) {
823
844
 
824
845
  case "pair_session_created":
825
846
  var createdPairGroup = handlePairCreated(msg);
826
- if (createdPairGroup) openGroup(createdPairGroup);
847
+ if (createdPairGroup) showCreatedWorkerPresentation(createdPairGroup);
827
848
  break;
828
849
 
829
850
  case "session_handoff_result":
@@ -898,6 +919,7 @@ export function processMessage(msg) {
898
919
 
899
920
  case "session_io":
900
921
  blinkSessionDot(msg.id);
922
+ handleWorkerSessionActivity(msg);
901
923
  break;
902
924
 
903
925
  case "session_unread":
@@ -60,6 +60,32 @@ var stickyBottomCeilingMs = 8000;
60
60
  var stickyBottomResizeObs = null;
61
61
  var stickyBottomInputBound = false;
62
62
 
63
+ function layoutScrollLock() { return store.get('paneLayoutScrollLock'); }
64
+
65
+ export function beginPaneLayoutScrollLock(token, scrollTop) {
66
+ var messagesEl = getMessagesEl();
67
+ if (!messagesEl || !token) return false;
68
+ disarmStickyBottom();
69
+ var userPosition = store.get('paneLastUserScrollTop');
70
+ var position = Number.isFinite(userPosition) ? userPosition : Number.isFinite(scrollTop) ? Math.max(0, scrollTop) : messagesEl.scrollTop;
71
+ store.set({ paneLayoutScrollLock: { token: token, scrollTop: position } });
72
+ messagesEl.scrollTop = position;
73
+ return true;
74
+ }
75
+
76
+ export function endPaneLayoutScrollLock(token) {
77
+ var state = layoutScrollLock(), messagesEl = getMessagesEl();
78
+ if (!state || state.token !== token || !messagesEl) return false;
79
+ requestAnimationFrame(function () {
80
+ var current = layoutScrollLock();
81
+ if (!current || current.token !== token || !messagesEl.isConnected) return;
82
+ disarmStickyBottom();
83
+ messagesEl.scrollTop = current.scrollTop;
84
+ store.set({ paneLayoutScrollLock: null });
85
+ });
86
+ return true;
87
+ }
88
+
63
89
  export function getStickyBottom() { return stickyBottom; }
64
90
 
65
91
  function pinToBottomNow() {
@@ -95,6 +121,7 @@ function ensureStickyInfrastructure() {
95
121
  }
96
122
 
97
123
  export function armStickyBottom(durationMs) {
124
+ if (layoutScrollLock()) return;
98
125
  if (prependAnchor) return;
99
126
  ensureStickyInfrastructure();
100
127
  stickyBottom = true;
@@ -128,6 +155,22 @@ export function disarmStickyBottom() {
128
155
  }
129
156
 
130
157
  export function initRendering() {
158
+ var messagesEl = getMessagesEl();
159
+ if (messagesEl && !messagesEl.__paneLayoutScrollLockListener) {
160
+ messagesEl.__paneLayoutScrollLockListener = true;
161
+ messagesEl.addEventListener('wheel', function () {
162
+ var revision = (store.get('paneUserScrollRevision') || 0) + 1;
163
+ store.set({ paneUserScrollRevision: revision, paneUserScrollIntent: revision });
164
+ requestAnimationFrame(function () {
165
+ if (store.get('paneUserScrollIntent') === revision) store.set({ paneUserScrollIntent: null });
166
+ });
167
+ }, { passive: true });
168
+ messagesEl.addEventListener('scroll', function () {
169
+ var state = layoutScrollLock();
170
+ if (state) store.set({ paneLayoutScrollLock: Object.assign({}, state, { scrollTop: messagesEl.scrollTop }) });
171
+ if (store.get('paneUserScrollIntent') != null) store.set({ paneLastUserScrollTop: messagesEl.scrollTop, paneUserScrollIntent: null });
172
+ }, { passive: true });
173
+ }
131
174
  var lastAssistantPlaceholder = '';
132
175
  function updateInputPlaceholder(state) {
133
176
  var inputEl = document.getElementById("input");
@@ -162,6 +205,7 @@ export function addToMessages(element) {
162
205
  if (prependAnchor) messagesEl.insertBefore(element, prependAnchor);
163
206
  else messagesEl.appendChild(element);
164
207
  keepActiveThinkingAtTail();
208
+ if (stickyBottomResizeObs && stickyBottom) stickyBottomResizeObs.observe(element);
165
209
  }
166
210
 
167
211
  export function addHistoryIndex(element, historyIndex) {
@@ -200,9 +244,15 @@ export function keepActiveThinkingAtTail() {
200
244
  }
201
245
 
202
246
  export function scrollToBottom() {
247
+ if (layoutScrollLock()) return;
203
248
  if (prependAnchor || store.get('replayingHistory')) return;
204
249
  var messagesEl = getMessagesEl();
205
250
  if (!messagesEl) return;
251
+ if (stickyBottom) {
252
+ pinToBottomNow();
253
+ requestAnimationFrame(function () { if (stickyBottom && !layoutScrollLock()) pinToBottomNow(); });
254
+ return;
255
+ }
206
256
  var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
207
257
  if (distFromBottom > 150 || isUserScrolledUp) {
208
258
  if (distFromBottom > 150) isUserScrolledUp = true;
@@ -214,6 +264,7 @@ export function scrollToBottom() {
214
264
  return;
215
265
  }
216
266
  requestAnimationFrame(function () {
267
+ if (layoutScrollLock()) return;
217
268
  var dist = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
218
269
  if (dist > 150) {
219
270
  isUserScrolledUp = true;
@@ -225,7 +276,10 @@ export function scrollToBottom() {
225
276
 
226
277
  export function forceScrollToBottom() {
227
278
  if (prependAnchor || store.get('replayingHistory')) return;
279
+ // An explicit new turn supersedes a layout-only scroll receipt.
280
+ if (layoutScrollLock()) store.set({ paneLayoutScrollLock: null });
228
281
  armStickyBottom(750);
282
+ requestAnimationFrame(function () { if (stickyBottom && !layoutScrollLock()) pinToBottomNow(); });
229
283
  }
230
284
 
231
285
  export function getMsgTime() {
@@ -8,6 +8,7 @@ var actions = {
8
8
  runtime: 'return this conversation to GUI chat', queue_add: 'queue the reviewed message for the Driver',
9
9
  queue_edit: 'update the reviewed queued message', queue_cancel: 'cancel the reviewed queued message',
10
10
  queue_reorder: 'reorder the reviewed message queue', navigate: 'open the reviewed conversation so we can continue there',
11
+ split_view: 'change the reviewed Split view',
11
12
  dismiss_notifications: 'dismiss the reviewed notifications', profile: 'update the reviewed Mate profile',
12
13
  send: 'deliver the reviewed message to this Mate', assign: 'prepare the requested Driver work for your review', follow_up: 'prepare a follow-up for the existing Driver', document: 'open the requested document for editing', sketch: 'present the requested sketch', browser: 'operate the requested shared browser tab'
13
14
  };
@@ -20,6 +21,8 @@ export function activityCopy(msg) {
20
21
  rendered: 'The reviewed conversation is visibly open in the selected window.',
21
22
  cancelled: 'I left the change unapplied.', failed: 'I could not finish this change. Please check the result before trying again.',
22
23
  partial: 'Part of this change took effect, but the operation failed. Please check the result.', unknown: 'I cannot confirm the outcome. Please check before trying again.' };
24
+ if (msg.domain === 'screen' && msg.action === 'split_view' && msg.phase === 'acknowledged') return 'I started opening the reviewed Split view. Its visible result is still unconfirmed.';
25
+ if (msg.domain === 'screen' && msg.action === 'split_view' && msg.phase === 'rendered') return 'The reviewed Split view and ready panes are visibly open in the selected window.';
23
26
  if (msg.domain === 'assignment') { var work = { review: 'I prepared the Driver work. Please review its proposal before it starts.', queued: 'I submitted the approved Driver work for dispatch.', running: 'The Driver is working on the approved request.', completed: 'The Driver finished this request.', cancelled: 'I left this Driver request unapplied.', unknown: 'I cannot confirm the Driver outcome. Please inspect the work before retrying.' }; return work[msg.phase] || endings[msg.phase] || null; }
24
27
  if (msg.domain === 'studio' && msg.phase === 'rendered') return 'The requested workbench content is visibly rendered in this window.';
25
28
  if (msg.domain === 'studio' && msg.phase === 'render_unknown') return 'The service returned, but I could not confirm that the workbench content is visible.';
@@ -1,7 +1,7 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
3
 
4
- function visible(element) {
4
+ export function visible(element) {
5
5
  if (!element || !element.isConnected || element.closest('[hidden], [inert], [aria-hidden="true"]')) return false;
6
6
  var box = element.getBoundingClientRect(), style = getComputedStyle(element);
7
7
  if (style.visibility !== 'visible' || Number(style.opacity) === 0 || box.width <= 0 || box.height <= 0) return false;
@@ -1,26 +1,34 @@
1
+ import { handleSplitViewProbe, splitViewScreenSnapshot, applySplitViewNavigation, observeSplitViewRendering } from './clay-split-view-screen.js';
1
2
  import { observeNavigationRendering } from './clay-navigation-render.js';
2
3
  import { showClayNavigationActivity, clearClayControlActivity } from './clay-control-activity.js';
3
4
  import { store } from './store.js';
4
5
  import { getWs } from './ws-ref.js';
5
6
  import { navigateToSession } from './command-palette.js';
7
+ var offerSocket = null;
6
8
  function context() {
7
- return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('dmMode'), store.get('homeShellVisible'), store.get('homeChatMateId'), store.get('homeChatSessionId'), store.get('homeSubSurface'), (store.get('contextualClay') || {}).mode]);
9
+ return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('dmMode'), store.get('homeShellVisible'), store.get('homeChatMateId'), store.get('homeChatSessionId'), store.get('homeSubSurface'), (store.get('contextualClay') || {}).mode, splitViewScreenSnapshot()]);
8
10
  }
9
11
  export function handleOwnerNavigation(msg) {
12
+ if (handleSplitViewProbe(msg)) return true;
10
13
  if (msg.type !== 'clay_navigation_offer' && msg.type !== 'clay_navigation_apply') return false;
11
14
  var ws = getWs();
12
15
  if (!ws || ws.readyState !== 1) return true;
13
16
  if (msg.type === 'clay_navigation_offer') {
14
17
  if (document.hidden || store.get('paneMode')) return true;
15
- store.set({ clayNavigationOffer: { id: msg.deliveryId, context: context(), socket: ws } });
18
+ offerSocket = ws;
19
+ store.set({ clayNavigationOffer: { id: msg.deliveryId, context: context() } });
16
20
  ws.send(JSON.stringify({ type: 'clay_navigation_claim', deliveryId: msg.deliveryId }));
17
21
  return true;
18
22
  }
19
23
  var offer = store.get('clayNavigationOffer');
20
24
  store.set({ clayNavigationOffer: null });
21
- var accepted = offer && offer.id === msg.deliveryId && offer.socket === ws && offer.context === context() && !document.hidden && !store.get('paneMode');
25
+ var accepted = offer && offer.id === msg.deliveryId && offerSocket === ws && offer.context === context() && !document.hidden && !store.get('paneMode');
26
+ offerSocket = null;
22
27
  var status = 'refused';
23
- if (accepted && msg.target && typeof msg.target.projectSlug === 'string' && Number.isSafeInteger(msg.target.sessionId)) {
28
+ if (accepted && msg.target && msg.target.surface === 'split_view') {
29
+ try { status = applySplitViewNavigation(msg.target) ? 'accepted' : 'refused'; } catch (error) { status = 'failed'; }
30
+ }
31
+ if (accepted && msg.target && msg.target.surface !== 'split_view' && typeof msg.target.projectSlug === 'string' && Number.isSafeInteger(msg.target.sessionId)) {
24
32
  try {
25
33
  showClayNavigationActivity(msg.target, false); navigateToSession(msg.target, null, msg.target.entryIndex); status = 'accepted';
26
34
  } catch (error) { showClayNavigationActivity(msg.target, true); status = 'failed'; }
@@ -28,6 +36,8 @@ export function handleOwnerNavigation(msg) {
28
36
  if (status === 'refused') clearClayControlActivity();
29
37
  // This acknowledges navigator invocation only. Rendering may finish after reconnect.
30
38
  if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'clay_navigation_ack', deliveryId: msg.deliveryId, status: status }));
31
- if (status === 'accepted') observeNavigationRendering(msg, ws);
39
+ if (status === 'accepted') {
40
+ if (msg.target.surface === 'split_view') observeSplitViewRendering(msg, ws); else observeNavigationRendering(msg, ws);
41
+ }
32
42
  return true;
33
43
  }
@@ -0,0 +1,74 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { livePaneBinding } from './pane-binding.js';
4
+ import { applyReviewedSplitView } from './split-view-presentations.js';
5
+ import { visible } from './clay-navigation-render.js';
6
+
7
+ var attempts = new Map();
8
+ export function splitViewScreenSnapshot() {
9
+ var state = store.snap();
10
+ var view = (state.projectSplitViews || []).find(function (item) { return item.id === state.projectSplitViewActiveId; });
11
+ var hidden = !!(document.hidden || state.paneMode || !state.connected || state.dmMode || state.homeShellVisible);
12
+ return { projectSlug: state.currentSlug || null, ownerId: state.myUserId || null, hidden: hidden,
13
+ viewId: view ? view.id : null, revision: view ? view.revision : 0, tree: view ? view.tree : null,
14
+ focusOrigin: state.projectSplitViewFocusedOrigin || null, singletonOrigin: state.projectSplitViewSingletonOrigin || null,
15
+ activeSessionId: state.activeSessionId || null,
16
+ panes: Array.from(document.querySelectorAll('.split-pane-frame')).slice(0, 32).map(function (frame) {
17
+ var live = frame.clayPane && frame.clayPane.identity(), binding = live && live.binding;
18
+ var ready = !!livePaneBinding(frame), cached = !!frame.closest('.split-pane-cached');
19
+ return { origin: frame.dataset.sessionOriginId || '', sessionId: Number(frame.dataset.sessionId) || 0,
20
+ visible: !hidden && !cached && visible(frame), cached: cached, ready: ready,
21
+ binding: ready && binding ? { requestId: binding.requestId, generation: binding.generation, sessionId: binding.sessionId, sessionOriginId: binding.sessionOriginId } : null };
22
+ }) };
23
+ }
24
+ function stableObservation(key, socket, deadline, complete, ready) {
25
+ if (attempts.has(key)) return;
26
+ if (attempts.size >= 32) return;
27
+ var animation, timer, previous, stable = 0, owner = store.get('myUserId'), slug = store.get('currentSlug'), ended = false;
28
+ function finish(snapshot) {
29
+ if (ended) return;
30
+ ended = true; clearTimeout(timer); cancelAnimationFrame(animation); attempts.delete(key);
31
+ document.removeEventListener('click', stop, true); document.removeEventListener('visibilitychange', hidden);
32
+ if (getWs() === socket && socket.readyState === 1) complete(snapshot);
33
+ }
34
+ function stop(event) { if (event.target.closest('#generation-stop, #autonomous-run-stop, #send-btn.stop')) finish(null); }
35
+ function hidden() { if (document.hidden) finish(null); }
36
+ document.addEventListener('click', stop, true); document.addEventListener('visibilitychange', hidden);
37
+ attempts.set(key, finish);
38
+ timer = setTimeout(function () { finish(null); }, Math.max(0, Math.min(5000, deadline - Date.now())));
39
+ function inspect() {
40
+ if (ended) return;
41
+ if (getWs() !== socket || !store.get('connected') || store.get('myUserId') !== owner || store.get('currentSlug') !== slug) { finish(null); return; }
42
+ var snapshot = splitViewScreenSnapshot(), serialized = JSON.stringify(snapshot);
43
+ stable = previous === serialized ? stable + 1 : 1; previous = serialized;
44
+ if (stable >= 2 && (!ready || ready(snapshot))) finish(snapshot); else animation = requestAnimationFrame(inspect);
45
+ }
46
+ animation = requestAnimationFrame(inspect);
47
+ }
48
+ export function handleSplitViewProbe(msg) {
49
+ if (msg.type !== 'clay_view_probe') return false;
50
+ var socket = getWs();
51
+ if (!socket || socket.readyState !== 1 || store.get('paneMode')) return true;
52
+ stableObservation(msg.probeId, socket, Date.now() + 4500, function (snapshot) {
53
+ socket.send(JSON.stringify({ type: 'clay_view_observed', probeId: msg.probeId, snapshot: snapshot }));
54
+ });
55
+ return true;
56
+ }
57
+ export function applySplitViewNavigation(target) {
58
+ if (JSON.stringify(splitViewScreenSnapshot()) !== JSON.stringify(target.expectedScreen)) return false;
59
+ return applyReviewedSplitView(target);
60
+ }
61
+ function renderedTarget(snapshot, target) {
62
+ var exact = snapshot && !snapshot.hidden && snapshot.viewId === target.view.id && snapshot.revision === target.view.revision &&
63
+ snapshot.focusOrigin === target.focusOrigin && JSON.stringify(snapshot.tree) === JSON.stringify(target.view.tree);
64
+ var expected = target.view.sessions.filter(function (session) { return !target.focusOrigin || session.sessionOriginId === target.focusOrigin; });
65
+ var shown = snapshot ? snapshot.panes.filter(function (pane) { return pane.visible; }) : [];
66
+ return !!(exact && shown.length === expected.length && expected.every(function (session) { return shown.some(function (pane) { return pane.origin === session.sessionOriginId && pane.sessionId === session.id && pane.ready && !pane.cached; }); }));
67
+ }
68
+ export function observeSplitViewRendering(msg, socket) {
69
+ var target = msg.target;
70
+ stableObservation(msg.deliveryId, socket, msg.renderDeadline, function (snapshot) {
71
+ socket.send(JSON.stringify({ type: 'clay_navigation_rendered', deliveryId: msg.deliveryId, renderToken: msg.renderToken,
72
+ surface: 'split_view', visible: renderedTarget(snapshot, target), snapshot: snapshot || splitViewScreenSnapshot() }));
73
+ }, function (snapshot) { return renderedTarget(snapshot, target); });
74
+ }
@@ -17,6 +17,11 @@ export function syncToolGroupDisclosure(el) {
17
17
  var expanded = !el.classList.contains('collapsed');
18
18
  el.querySelector('.tool-group-header').setAttribute('aria-expanded', String(expanded));
19
19
  var items = el.querySelector('.tool-group-items');
20
+ if (el.classList.contains('popup-chat-activity') && typeof document !== 'undefined' && document.body && document.body.classList.contains('popup-chat-rendering')) {
21
+ items.inert = false;
22
+ items.setAttribute('aria-hidden', 'false');
23
+ return;
24
+ }
20
25
  items.inert = !expanded;
21
26
  items.setAttribute('aria-hidden', String(!expanded));
22
27
  }
@@ -1,3 +1,4 @@
1
+ import { formatRelativeDate, paletteSelectionIndex } from './palette-render-helpers.js';
1
2
  import { forwardContextualSessionCitation, validContextualCitation, contextualCitationLink } from './contextual-session-citations.js';
2
3
  import { mateAvatarUrl } from './avatar.js';
3
4
  import { escapeHtml } from './utils.js';
@@ -7,23 +8,10 @@ import { startSearchClayChat, retrySearchClayAsk } from './search-clay-chat.js';
7
8
  import { getWs } from './ws-ref.js';
8
9
  import { openProjectLog } from './project-logs.js';
9
10
  import { store } from './store.js';
11
+ import { initSearchHints, setSearchHintsPaletteOpen } from './search-hints.js';
12
+ import { openPaletteClayConversation } from './palette-clay-conversation.js';
10
13
  import { disarmStickyBottom } from './chat-render-runtime.js';
11
14
 
12
- function formatRelativeDate(ts) {
13
- if (!ts) return "";
14
- var diff = Math.max(0, Date.now() - ts);
15
- var min = Math.floor(diff / 60000);
16
- if (min < 1) return "just now";
17
- if (min < 60) return min + "m ago";
18
- var hr = Math.floor(min / 60);
19
- if (hr < 24) return hr + "h ago";
20
- var days = Math.floor(hr / 24);
21
- if (days < 7) return days + "d ago";
22
- if (days < 30) return Math.floor(days / 7) + "w ago";
23
- var d = new Date(ts);
24
- var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
25
- return months[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
26
- }
27
15
 
28
16
  var ctx;
29
17
  var paletteEl = null, inputEl = null, resultsEl = null, footerEl = null;
@@ -34,12 +22,14 @@ var searchResults = [], searchQuery = "", searchPending = false;
34
22
 
35
23
  export function initCommandPalette(value) {
36
24
  ctx = value;
25
+ if (paletteEl) return;
37
26
  buildDOM();
38
27
  // The palette dialog stops bubbling so backdrop clicks cannot close it.
39
28
  // Capture session controls before that boundary; the same handler also
40
29
  // serves links rendered in the full Home transcript.
41
30
  document.addEventListener("click", handleClaySessionLinkClick, true);
42
31
  var trigger = document.getElementById("cmd-palette-btn");
32
+ initSearchHints(trigger, inputEl);
43
33
  if (trigger) {
44
34
  var isMac = navigator.platform.indexOf("Mac") !== -1;
45
35
  var kbd = trigger.querySelector(".cmd-palette-searchbar-kbd");
@@ -154,6 +144,7 @@ export function isCommandPaletteOpen() { return paletteEl && !paletteEl.classLis
154
144
  export function openCommandPalette() {
155
145
  if (!paletteEl) return;
156
146
  paletteEl.classList.remove("hidden");
147
+ setSearchHintsPaletteOpen(true);
157
148
  var trigger = document.getElementById("cmd-palette-btn");
158
149
  if (trigger) trigger.style.visibility = "hidden";
159
150
  updateFooter();
@@ -162,8 +153,10 @@ export function openCommandPalette() {
162
153
  }
163
154
 
164
155
  export function closeCommandPalette() {
156
+ if ((store.get('paletteClayConversation') || {}).phase === 'resolving') store.set({ paletteClayConversation: null });
165
157
  if (!paletteEl) return;
166
158
  paletteEl.classList.add("hidden");
159
+ setSearchHintsPaletteOpen(false);
167
160
  var trigger = document.getElementById("cmd-palette-btn");
168
161
  if (trigger) trigger.style.visibility = "";
169
162
  if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = null; }
@@ -297,7 +290,7 @@ function buildDOM() {
297
290
  '<div class="cmd-palette-input-row">' +
298
291
  '<img class="cmd-palette-input-brand" src="/clay-studio-symbol.png" width="22" height="22" alt="">' +
299
292
  '<i data-lucide="search" aria-hidden="true"></i>' +
300
- '<input class="cmd-palette-input" type="text" placeholder="Search conversations, or ask Clay…" autocomplete="off" spellcheck="false" />' +
293
+ '<input class="cmd-palette-input" type="text" aria-label="Search or ask Clay" placeholder="Search or ask Clay, like “What was I working on recently?”" autocomplete="off" spellcheck="false" />' +
301
294
  '<button type="button" class="cmd-palette-kbd" id="cmd-palette-close" aria-label="Close search"><i data-lucide="x"></i></button>' +
302
295
  '</div>' +
303
296
  '<div class="cmd-palette-results"></div>' +
@@ -313,11 +306,13 @@ function buildDOM() {
313
306
  inputEl.addEventListener("input", function () {
314
307
  var query = inputEl.value.trim();
315
308
  searchQuery = query;
309
+ if (abortCtrl) { abortCtrl.abort(); abortCtrl = null; }
316
310
  searchResults = [];
317
311
  searchPending = !!query;
318
312
  renderSearch();
319
313
  if (debounceTimer) clearTimeout(debounceTimer);
320
314
  if (query) debounceTimer = setTimeout(function () { fetchHomeSearchResults(query); }, 250);
315
+ else fetchHomeData();
321
316
  });
322
317
  inputEl.addEventListener("keydown", function (event) {
323
318
  if (event.key === "Escape") { event.preventDefault(); closeCommandPalette(); return; }
@@ -347,13 +342,15 @@ function updateFooter() {
347
342
  function fetchHomeData() {
348
343
  if (cachedHomeData.length) { renderSearch(); return; }
349
344
  if (abortCtrl) abortCtrl.abort();
350
- abortCtrl = new AbortController();
345
+ var request = abortCtrl = new AbortController();
351
346
  fetch("/api/palette/search", { signal: abortCtrl.signal }).then(function (res) { return res.json(); }).then(function (data) {
347
+ if (abortCtrl !== request) return;
352
348
  abortCtrl = null;
353
349
  cachedHomeData = data.results || [];
354
350
  renderSearch();
355
351
  }).catch(function (error) {
356
352
  if (error.name === "AbortError") return;
353
+ if (abortCtrl !== request) return;
357
354
  abortCtrl = null;
358
355
  cachedHomeData = [];
359
356
  renderSearch();
@@ -362,8 +359,9 @@ function fetchHomeData() {
362
359
 
363
360
  function fetchHomeSearchResults(query) {
364
361
  if (abortCtrl) abortCtrl.abort();
365
- abortCtrl = new AbortController();
362
+ var request = abortCtrl = new AbortController();
366
363
  fetch("/api/palette/search?q=" + encodeURIComponent(query), { signal: abortCtrl.signal }).then(function (res) { return res.json(); }).then(function (data) {
364
+ if (abortCtrl !== request) return;
367
365
  abortCtrl = null;
368
366
  if (searchQuery !== query) return;
369
367
  searchResults = data.results || [];
@@ -371,6 +369,7 @@ function fetchHomeSearchResults(query) {
371
369
  renderSearch();
372
370
  }).catch(function (error) {
373
371
  if (error.name === "AbortError") return;
372
+ if (abortCtrl !== request) return;
374
373
  abortCtrl = null;
375
374
  if (searchQuery !== query) return;
376
375
  searchPending = false;
@@ -396,6 +395,7 @@ function sessionIcon(session) {
396
395
  }
397
396
 
398
397
  function renderSearch() {
398
+ var previous = items[activeIndex];
399
399
  var query = searchQuery.toLowerCase();
400
400
  var sessions = query ? cachedHomeData.filter(function (item) {
401
401
  return (item.sessionTitle || "").toLowerCase().indexOf(query) !== -1 || (item.projectTitle || "").toLowerCase().indexOf(query) !== -1;
@@ -410,6 +410,11 @@ function renderSearch() {
410
410
  items.push({ type: 'retry-clay-ask' });
411
411
  html += renderItem(index++, '<img src="/clay-studio-symbol.png" width="22" height="22" alt="">', 'Retry previous question', escapeHtml(pendingAsk.text), null, null, ' is-retry-clay-ask');
412
412
  }
413
+ if (query) {
414
+ items.push({ type: "ask-clay", query: searchQuery });
415
+ html += '<div class="cmd-palette-group-label">Ask Clay</div>';
416
+ html += renderItem(index++, '<img src="/clay-studio-symbol.png" width="22" height="22" alt="">', 'Ask Clay about “' + escapeHtml(searchQuery) + '”', "Start a new Clay conversation", null, null, " is-ask-clay");
417
+ }
413
418
  if (sessions.length) {
414
419
  html += '<div class="cmd-palette-group-label">' + (query ? "Conversations" : "Recent conversations") + '</div>';
415
420
  for (var s = 0; s < sessions.length; s++) {
@@ -420,17 +425,13 @@ function renderSearch() {
420
425
  html += renderItem(index++, sessionIcon(session), escapeHtml(session.sessionTitle || "New Chat"), project, snippet, session.lastActivity, "");
421
426
  }
422
427
  }
423
- if (query) {
424
- items.push({ type: "ask-clay", query: searchQuery });
425
- if (!sessions.length && !searchPending) html += '<div class="cmd-palette-exact-empty">No exact matches. Clay can search by meaning.</div>';
426
- html += '<div class="cmd-palette-group-label">Ask Clay</div>';
427
- html += renderItem(index, '<img src="/clay-studio-symbol.png" width="22" height="22" alt="">', 'Ask Clay about “' + escapeHtml(searchQuery) + '”', "Start a new Clay conversation", null, null, " is-ask-clay");
428
- }
428
+ if (query && !sessions.length && !searchPending) html += '<div class="cmd-palette-exact-empty">No exact matches. Clay can search by meaning.</div>';
429
429
  if (!html) html = '<div class="cmd-palette-empty">No conversations yet.</div>';
430
430
  resultsEl.innerHTML = html;
431
- activeIndex = -1;
431
+ activeIndex = paletteSelectionIndex(previous, items, query);
432
432
  refreshIcons();
433
433
  bindItemEvents();
434
+ if (activeIndex >= 0) setActive(activeIndex, true);
434
435
  }
435
436
 
436
437
  function renderItem(index, icon, title, desc, snippet, timestamp, extraClass) {
@@ -455,16 +456,17 @@ function setActive(index, skipScroll) {
455
456
  for (var i = 0; i < elements.length; i++) elements[i].classList.toggle("active", i === index);
456
457
  if (!skipScroll && elements[index]) elements[index].scrollIntoView({ block: "nearest" });
457
458
  }
458
-
459
459
  function beginClayChat(query) {
460
460
  if (!isCommandPaletteOpen()) return;
461
461
  if (startSearchClayChat(query)) closeCommandPalette();
462
462
  }
463
-
464
463
  function activateItem(entry) {
465
464
  if (entry.type === 'retry-clay-ask') { if (retrySearchClayAsk()) closeCommandPalette(); return; }
466
465
  if (entry.type === "ask-clay") { beginClayChat(entry.query); return; }
467
- if (entry.type === "mate-session" || entry.type === "session") navigateToSession(entry.data, entry.query);
466
+ if (entry.type === "mate-session" || entry.type === "session") {
467
+ if (!store.get('paneMode') && (entry.data.isMate || /^mate-/.test(entry.data.projectSlug)) && openPaletteClayConversation(entry.data, entry.query, entry.data.entryIndex)) return;
468
+ navigateToSession(entry.data, entry.query, entry.data.entryIndex);
469
+ }
468
470
  }
469
471
 
470
472
  export function navigateToSession(item, query, entryIndex) {
@@ -1,16 +1,28 @@
1
1
  // Session identity remains pinned across transport recovery and presentation.
2
+ import { navigateToSession } from './command-palette.js';
2
3
  import { autoResize, takeComposerDraft, restoreComposerDraft } from './input.js';
3
4
  import { store } from './store.js';
4
5
  import { withdrawPaneControlActivity } from './clay-control-activity.js';
5
6
  import { initContextualComposer } from './contextual-composer.js';
6
7
  import { submitContextualAsk, pauseContextualAsk, stopContextualAsk } from './contextual-ask-session.js';
8
+ import { resolveProjectAssistantIdentity } from './project-chat-identity.js';
9
+ import { setPopupChatRendering, popupChatContextIsReady } from './popup-chat-rendering.js';
10
+ import { syncPopupChatToolGroups } from './tools.js';
7
11
 
8
12
  // Session replay and authenticated account bootstrap complete independently.
9
13
  function submissionReady() { var expected = window.frameElement && window.frameElement.dataset.expectedSessionOrigin; return (!expected || expected === store.get('sessionOriginId')) && !!(store.get('contextualSessionReady') && store.get('connected') && store.get('myUserId') && store.get('sessionOriginId') && store.get('activeSessionId') === store.get('paneSessionId')); }
10
14
  function enabled() { return store.get('paneMode') && new URLSearchParams(location.search).get('contextual') === '1'; }
11
- function renderSessionAvailability() {
15
+ function renderSessionAvailability(notifyReady) {
12
16
  var input = document.getElementById('input');
13
17
  var frame = window.frameElement;
18
+ var state = store.snap();
19
+ var identity = resolveProjectAssistantIdentity();
20
+ if (state.contextualSessionReady && frame && !frame.hidden) frame.dataset.sessionId = String(state.paneSessionId);
21
+ var popupSurface = null;
22
+ try { popupSurface = window.parent.document.getElementById('contextual-clay'); } catch (error) { /* Popup rendering stays native outside its authenticated parent surface. */ }
23
+ var popupReady = popupChatContextIsReady(document.body, state, frame, identity, popupSurface);
24
+ setPopupChatRendering(popupReady);
25
+ syncPopupChatToolGroups(popupReady);
14
26
  var mate = frame && (store.get('cachedMatesList') || []).find(function (item) { return item.id === frame.dataset.mateId; });
15
27
  var name = mate && ((mate.profile || {}).displayName || mate.displayName || mate.name);
16
28
  if (input && name) { input.setAttribute('aria-label', 'Message ' + name); if (name.length > 32) input.placeholder = 'Message this Mate…'; }
@@ -24,7 +36,7 @@ function renderSessionAvailability() {
24
36
  if (surface.dataset.mode === 'popover' && window.parent.document.activeElement === anchor) document.getElementById('input').focus({ preventScroll: true });
25
37
  }
26
38
  } catch (error) { /* Cross-origin authentication owns focus. */ }
27
- if (store.get('contextualSessionReady') && window.frameElement && !window.frameElement.hidden) {
39
+ if (notifyReady !== false && store.get('contextualSessionReady') && window.frameElement && !window.frameElement.hidden) {
28
40
  window.frameElement.dataset.sessionId = String(store.get('paneSessionId'));
29
41
  var parentSurface = window.parent.document.getElementById('contextual-clay');
30
42
  if (parentSurface) parentSurface.dispatchEvent(new window.parent.CustomEvent('contextual-clay-ready'));
@@ -67,8 +79,20 @@ export function initContextualSession() {
67
79
  } catch (error) { /* Authentication navigation owns its document. */ }
68
80
  });
69
81
  var surfaceBridge = window.parent.document.getElementById('contextual-clay');
82
+ if (window.frameElement && surfaceBridge && typeof MutationObserver !== 'undefined') {
83
+ // Visibility changes update presentation and focus, never session readiness.
84
+ // A ready notification renders the parent, which can mutate these attributes.
85
+ var popupVisibilityObserver = new MutationObserver(function () { renderSessionAvailability(false); });
86
+ popupVisibilityObserver.observe(window.frameElement, { attributes: true, attributeFilter: ['hidden', 'inert'] });
87
+ popupVisibilityObserver.observe(surfaceBridge, { attributes: true, attributeFilter: ['data-mode'] });
88
+ }
70
89
  if (surfaceBridge && window.frameElement && surfaceBridge.contains(window.frameElement)) window.frameElement.conversation = {
71
90
  source: window, document: document,
91
+ navigateEntry: function (target) {
92
+ if (!submissionReady() || target.ownerId !== store.get('myUserId') || target.mateId !== store.get('activeProjectMateId') || target.sessionId !== store.get('paneSessionId') || target.sessionOriginId !== store.get('sessionOriginId')) return false;
93
+ navigateToSession({ projectSlug: store.get('currentSlug'), sessionId: target.sessionId }, target.query, target.entryIndex);
94
+ return true;
95
+ },
72
96
  submitAsk: submitContextualAsk,
73
97
  pauseAsk: pauseContextualAsk,
74
98
  stopAsk: stopContextualAsk,
@@ -61,7 +61,7 @@ function render() {
61
61
  renderContextualWorkspaceBack();
62
62
  if (!surface) return;
63
63
  surface.hidden = mode === 'closed' || mode === 'workspace';
64
- surface.dataset.mode = mode;
64
+ if (surface.dataset.mode !== mode) surface.dataset.mode = mode;
65
65
  positionContextualClay();
66
66
  surface.querySelector('[data-clay-expand]').hidden = mode !== 'popover';
67
67
  surface.querySelector('[data-clay-close]').hidden = mode !== 'popover' && mode !== 'opening';
@@ -100,7 +100,10 @@ function render() {
100
100
  var selectedFrame = contextualFrame();
101
101
  var connection = selectedFrame && selectedFrame.conversation;
102
102
  surface.querySelector('[data-clay-loading]').hidden = !!(connection && connection.identity().ready) || surface.dataset.askPending === 'true';
103
- surface.querySelectorAll('iframe').forEach(function (frame) { frame.hidden = frame !== selectedFrame; });
103
+ surface.querySelectorAll('iframe').forEach(function (frame) {
104
+ var hidden = frame !== selectedFrame;
105
+ if (frame.hidden !== hidden) frame.hidden = hidden;
106
+ });
104
107
  }
105
108
 
106
109
  function createFrame(mate, sessionId) {