clay-server 5.2.0-beta.8 → 5.2.0

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 (143) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/pane-pending-interactions.js +20 -0
  9. package/lib/pane-session-activation.js +114 -0
  10. package/lib/pane-session-transport.js +26 -0
  11. package/lib/project-session-notes.js +33 -8
  12. package/lib/project-sessions.js +78 -29
  13. package/lib/project-start-protocol.js +29 -0
  14. package/lib/project-vendor-login.js +2 -0
  15. package/lib/project.js +16 -4
  16. package/lib/public/app.js +41 -18
  17. package/lib/public/css/contextual-clay.css +2 -2
  18. package/lib/public/css/filebrowser.css +1 -1
  19. package/lib/public/css/input.css +6 -0
  20. package/lib/public/css/mate-loading-scene.css +21 -0
  21. package/lib/public/css/overlays.css +64 -256
  22. package/lib/public/css/project-mate-creation.css +5 -6
  23. package/lib/public/css/project-start.css +108 -0
  24. package/lib/public/css/projects-hub.css +53 -14
  25. package/lib/public/css/settings-shell.css +103 -0
  26. package/lib/public/css/sidebar.css +7 -0
  27. package/lib/public/css/sticky-notes.css +37 -11
  28. package/lib/public/css/title-bar.css +83 -0
  29. package/lib/public/index.html +50 -63
  30. package/lib/public/modules/account-transport-settings.js +8 -2
  31. package/lib/public/modules/admin-settings-email.js +7 -2
  32. package/lib/public/modules/admin-settings-invitations.js +2 -2
  33. package/lib/public/modules/admin-settings-projects.js +2 -2
  34. package/lib/public/modules/admin-settings-ui.js +20 -4
  35. package/lib/public/modules/admin-settings-users.js +4 -4
  36. package/lib/public/modules/app-connection.js +3 -0
  37. package/lib/public/modules/app-home-hub.js +2 -1
  38. package/lib/public/modules/app-message-router.js +13 -0
  39. package/lib/public/modules/app-messages.js +35 -7
  40. package/lib/public/modules/app-misc.js +3 -1
  41. package/lib/public/modules/app-projects.js +6 -407
  42. package/lib/public/modules/app-rate-limit.js +12 -1
  43. package/lib/public/modules/clay-owner-navigation.js +0 -3
  44. package/lib/public/modules/clay-studio-render.js +3 -1
  45. package/lib/public/modules/clay-tool-presentation.js +1 -0
  46. package/lib/public/modules/command-palette.js +52 -14
  47. package/lib/public/modules/contextual-clay-workspace.js +14 -2
  48. package/lib/public/modules/contextual-clay.js +19 -4
  49. package/lib/public/modules/contextual-mate-context.js +6 -4
  50. package/lib/public/modules/elicitation-form.js +5 -0
  51. package/lib/public/modules/github-star.js +66 -0
  52. package/lib/public/modules/history-replay-batch.js +8 -0
  53. package/lib/public/modules/home-surface-boot.js +0 -1
  54. package/lib/public/modules/input.js +55 -11
  55. package/lib/public/modules/mate-loading-scene.js +17 -0
  56. package/lib/public/modules/message-delivery.js +10 -2
  57. package/lib/public/modules/model-picker.js +2 -0
  58. package/lib/public/modules/pane-activation.js +214 -0
  59. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  60. package/lib/public/modules/pane-binding.js +82 -0
  61. package/lib/public/modules/pane-bridge.js +5 -2
  62. package/lib/public/modules/pane-browser-bridge.js +5 -3
  63. package/lib/public/modules/pane-file-bridge.js +4 -0
  64. package/lib/public/modules/pane-issue-bridge.js +4 -1
  65. package/lib/public/modules/profile.js +11 -3
  66. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  67. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  68. package/lib/public/modules/project-settings-environment.js +12 -1
  69. package/lib/public/modules/project-settings.js +119 -129
  70. package/lib/public/modules/project-start-dialog.js +130 -0
  71. package/lib/public/modules/project-start-folder-state.js +78 -0
  72. package/lib/public/modules/project-start-folder-view.js +190 -0
  73. package/lib/public/modules/project-start-forms.js +66 -0
  74. package/lib/public/modules/project-start-path.js +60 -0
  75. package/lib/public/modules/project-start-state.js +54 -0
  76. package/lib/public/modules/project-start-submit.js +67 -0
  77. package/lib/public/modules/projects-hub-view.js +32 -44
  78. package/lib/public/modules/projects-hub.js +39 -28
  79. package/lib/public/modules/rewind.js +5 -0
  80. package/lib/public/modules/server-settings.js +36 -91
  81. package/lib/public/modules/session-actions.js +5 -2
  82. package/lib/public/modules/session-context-menu.js +5 -0
  83. package/lib/public/modules/session-rich-drafts.js +21 -0
  84. package/lib/public/modules/session-switch-defer.js +67 -0
  85. package/lib/public/modules/settings-project-target.js +56 -0
  86. package/lib/public/modules/settings-select.js +1 -1
  87. package/lib/public/modules/settings-shell-registry.js +88 -0
  88. package/lib/public/modules/settings-shell-view.js +104 -0
  89. package/lib/public/modules/settings-shell.js +342 -0
  90. package/lib/public/modules/sidebar-mobile.js +8 -30
  91. package/lib/public/modules/sidebar-projects.js +2 -1
  92. package/lib/public/modules/split-group-restore.js +68 -0
  93. package/lib/public/modules/split-pair-ui.js +1 -0
  94. package/lib/public/modules/split-pane-reconciler.js +15 -0
  95. package/lib/public/modules/split-pane-renderer.js +87 -6
  96. package/lib/public/modules/split-session-boundary.js +1 -1
  97. package/lib/public/modules/split-view.js +51 -52
  98. package/lib/public/modules/sticky-notes-checklist.js +51 -6
  99. package/lib/public/modules/stt-controller.js +4 -1
  100. package/lib/public/modules/stt-recording.js +1 -1
  101. package/lib/public/modules/theme.js +31 -3
  102. package/lib/public/modules/tools.js +18 -2
  103. package/lib/public/modules/user-settings-integrations.js +2 -1
  104. package/lib/public/modules/user-settings-layout.js +12 -34
  105. package/lib/public/modules/user-settings-profile.js +1 -1
  106. package/lib/public/modules/user-settings.js +18 -85
  107. package/lib/public/modules/vendor-usage-format.js +93 -0
  108. package/lib/public/modules/vendor-usage-list.js +45 -0
  109. package/lib/public/modules/vendor-usage-model.js +215 -0
  110. package/lib/public/modules/vendor-usage-overview.js +323 -0
  111. package/lib/public/style.css +10 -7
  112. package/lib/public/welcome-art/conversation-desk.png +0 -0
  113. package/lib/public/welcome-art/mate-clay.png +0 -0
  114. package/lib/public/welcome-art/project-clay.png +0 -0
  115. package/lib/sdk-bridge.js +6 -1
  116. package/lib/sdk-message-processor.js +9 -1
  117. package/lib/sdk-quota-snapshot.js +20 -2
  118. package/lib/server-github-stars.js +39 -0
  119. package/lib/server-global-ws.js +21 -17
  120. package/lib/server.js +11 -0
  121. package/lib/session-notes-mcp-server.js +26 -4
  122. package/lib/session-notes-writes.js +88 -0
  123. package/lib/session-visibility.js +20 -8
  124. package/lib/sessions.js +27 -10
  125. package/lib/vendor-usage-acp-readers.js +97 -0
  126. package/lib/vendor-usage-capabilities.js +88 -0
  127. package/lib/vendor-usage-http-readers.js +214 -0
  128. package/lib/vendor-usage-metrics.js +47 -0
  129. package/lib/vendor-usage-native-readers.js +106 -0
  130. package/lib/vendor-usage-overview.js +406 -0
  131. package/lib/vendor-usage-service.js +281 -0
  132. package/lib/ws-schema.js +4 -2
  133. package/lib/yoke/adapters/claude.js +12 -0
  134. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  135. package/lib/yoke/adapters/codex.js +45 -12
  136. package/lib/yoke/claude-hook-installer.js +1 -0
  137. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  138. package/lib/yoke/codex-model-catalog.js +37 -3
  139. package/lib/yoke/muse-services.js +16 -3
  140. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  141. package/lib/yoke/vendor-registry.js +6 -2
  142. package/package.json +5 -3
  143. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -8,6 +8,7 @@ import { setRewindMode, isRewindMode } from './rewind.js';
8
8
  import { renderPicker as renderContextPicker } from './context-sources.js';
9
9
  import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible, mentionMenuKeydown, setMentionAtIdx, parseMentionFromInput, clearMentionState, stickyReapplyMention, sendMention, sendUserMention, renderMentionUser, renderUserMention, removeMentionChip } from './mention.js';
10
10
  import { store } from './store.js';
11
+ import { sessionCallback, paneMessageBinding, samePaneBinding } from './pane-binding.js';
11
12
  import { sendAcknowledgedMessage } from './message-delivery.js';
12
13
  import { mateAvatarUrl } from './avatar.js';
13
14
  import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
@@ -122,6 +123,9 @@ function showPreThinkingForTurn() {
122
123
 
123
124
  // --- Send ---
124
125
  export function sendMessage() {
126
+ if (store.get('deferredSessionSwitch') || store.get('panePendingBinding') || store.get('paneSuspended')) return;
127
+ if (store.get('paneCommittedBinding') && !store.get('paneReady')) return;
128
+ var actionBinding = paneMessageBinding();
125
129
  if (store.get('contextualWriterSuspended')) return;
126
130
  if (ctx && requestRecordedSend(ctx.inputEl, sendMessage)) return;
127
131
  stopSTT();
@@ -346,6 +350,7 @@ export function sendMessage() {
346
350
  store.set({ pendingScheduleAdmissions: admissions, pendingScheduleAdmission: admissions[scheduleRequestId] });
347
351
  ctx.ws.send(JSON.stringify({
348
352
  type: "schedule_message", requestId: scheduleRequestId,
353
+ paneBinding: actionBinding || undefined,
349
354
  projectSlug: scheduleContext.projectSlug, sessionId: scheduleContext.sessionId, accountId: scheduleContext.accountId,
350
355
  text: text || "", images: images, pastes: pastes, resetsAt: resetsAt,
351
356
  }));
@@ -354,6 +359,7 @@ export function sendMessage() {
354
359
 
355
360
  var screenReference = contextualScreenReference();
356
361
  var payload = { type: "message", text: text || "" };
362
+ if (actionBinding) payload.paneBinding = actionBinding;
357
363
  if (screenReference) payload.contextualSnapshot = screenReference;
358
364
  if (images.length > 0) {
359
365
  payload.images = images;
@@ -440,6 +446,7 @@ export function sendTextMessage(text) {
440
446
  export function sendShellResultToAgent(msg) {
441
447
  if (!ctx || !ctx.connected || !msg || msg.error || !msg.command) return false;
442
448
  if (String(msg.sessionId) !== String(store.get("activeSessionId"))) return false;
449
+ if (store.get('paneCommittedBinding') && !samePaneBinding(msg.paneActivation, store.get('paneCommittedBinding'))) return false;
443
450
  var payload = {
444
451
  type: "message",
445
452
  text: "$ " + msg.command,
@@ -447,7 +454,8 @@ export function sendShellResultToAgent(msg) {
447
454
  };
448
455
  var selectedVendor = store.get("currentVendor") || null;
449
456
  if (selectedVendor) payload.vendor = selectedVendor;
450
- ctx.ws.send(JSON.stringify(payload));
457
+ if (msg.paneActivation) payload.paneBinding = msg.paneActivation;
458
+ if (ctx.ws.send(JSON.stringify(payload)) === false) return false;
451
459
  commitVendorForTurn();
452
460
  showPreThinkingForTurn();
453
461
  return true;
@@ -543,9 +551,17 @@ function removePendingImage(idx) {
543
551
  }
544
552
 
545
553
  // Move composer ownership between two authenticated views of the same session.
554
+ export function composerDepartureReason() {
555
+ if (uploadingCount) return 'Wait for attachments to finish uploading before changing views.';
556
+ if (store.get('speechActive') || ctx && hasRecordedSpeechFor(ctx.inputEl)) return 'Finish or discard the voice recording before changing views.';
557
+ var retry = store.get('speechRetry');
558
+ if (retry && ctx && retry.input === ctx.inputEl) return 'Retry or discard the voice recording before changing views.';
559
+ return '';
560
+ }
546
561
  export function takeComposerDraft() {
547
- if (uploadingCount) throw new Error('Wait for attachments to finish uploading before opening Clay.');
548
- if (store.get('speechActive') || hasRecordedSpeechFor(ctx.inputEl)) throw new Error('Finish or discard the voice recording before changing Clay views.');
562
+ var reason = composerDepartureReason();
563
+ if (reason) throw new Error(reason);
564
+ if (document.dispatchEvent) document.dispatchEvent(new CustomEvent('clay-composer-handoff'));
549
565
  var input = ctx.inputEl;
550
566
  var draft = { text: input.value, start: input.selectionStart, end: input.selectionEnd, direction: input.selectionDirection, images: pendingImages, pastes: pendingPastes, files: pendingFiles, delay: scheduleDelayMs };
551
567
  input.value = '';
@@ -585,14 +601,14 @@ function pastePreview(text) {
585
601
  }
586
602
 
587
603
  function editPendingPaste(idx) {
588
- showPasteModal(pendingPastes[idx].text, function (text) {
604
+ showPasteModal(pendingPastes[idx].text, sessionCallback(function (text) {
589
605
  if (!text) {
590
606
  removePendingPaste(idx);
591
607
  return;
592
608
  }
593
609
  pendingPastes[idx] = { text: text, preview: pastePreview(text) };
594
610
  renderInputPreviews();
595
- });
611
+ }));
596
612
  }
597
613
 
598
614
  function removePendingFile(idx) {
@@ -726,6 +742,7 @@ function uploadFile(file) {
726
742
  return;
727
743
  }
728
744
  uploadingCount++;
745
+ store.set({ composerUploadingCount: uploadingCount });
729
746
  renderInputPreviews();
730
747
  var reader = new FileReader();
731
748
  reader.onload = function (ev) {
@@ -738,12 +755,14 @@ function uploadFile(file) {
738
755
  xhr.setRequestHeader("Content-Type", "application/json");
739
756
  xhr.onload = function () {
740
757
  uploadingCount--;
758
+ store.set({ composerUploadingCount: uploadingCount });
741
759
  if (xhr.status === 200) {
742
760
  try {
743
761
  var resp = JSON.parse(xhr.responseText);
744
762
  pendingFiles.push({ name: resp.name || uploadName, path: resp.path });
745
- } catch (e) {}
763
+ } catch (e) { showToast('Upload response could not be read: ' + uploadName + '. Your draft is preserved.', 'error'); }
746
764
  } else {
765
+ showToast('Upload failed: ' + uploadName + '. Your draft is preserved.', 'error');
747
766
  if (ctx.addSystemMessage) ctx.addSystemMessage("Upload failed: " + uploadName, true);
748
767
  }
749
768
  renderInputPreviews();
@@ -753,6 +772,8 @@ function uploadFile(file) {
753
772
  };
754
773
  xhr.onerror = function () {
755
774
  uploadingCount--;
775
+ store.set({ composerUploadingCount: uploadingCount });
776
+ showToast('Upload failed: ' + uploadName + '. Your draft is preserved.', 'error');
756
777
  if (ctx.addSystemMessage) ctx.addSystemMessage("Upload failed: " + uploadName, true);
757
778
  renderInputPreviews();
758
779
  if (ctx.processing && ctx.setSendBtnMode) {
@@ -761,6 +782,7 @@ function uploadFile(file) {
761
782
  };
762
783
  xhr.send(JSON.stringify({ name: uploadName, data: b64 }));
763
784
  };
785
+ reader.onerror = function () { uploadingCount--; store.set({ composerUploadingCount: uploadingCount }); renderInputPreviews(); showToast('Attachment could not be read. Your draft is preserved.', 'error'); };
764
786
  reader.readAsDataURL(file);
765
787
  }
766
788
 
@@ -772,7 +794,14 @@ function readImageBlob(blob) {
772
794
  uploadFile(blob);
773
795
  return;
774
796
  }
797
+ uploadingCount++; store.set({ composerUploadingCount: uploadingCount });
798
+ var completed = false;
799
+ function finish() {
800
+ if (completed) return; completed = true; uploadingCount--;
801
+ store.set({ composerUploadingCount: uploadingCount }); renderInputPreviews();
802
+ }
775
803
  var reader = new FileReader();
804
+ reader.onerror = function () { finish(); showToast('Image could not be read. Your draft is preserved.', 'error'); };
776
805
  reader.onload = function (ev) {
777
806
  var dataUrl = ev.target.result;
778
807
  // Check base64 payload size (~3/4 of base64 length)
@@ -781,7 +810,7 @@ function readImageBlob(blob) {
781
810
  var estimatedBytes = b64.length * 0.75;
782
811
 
783
812
  if (estimatedBytes <= MAX_IMAGE_BYTES) {
784
- addPendingImage(dataUrl);
813
+ addPendingImage(dataUrl); finish();
785
814
  return;
786
815
  }
787
816
 
@@ -799,8 +828,9 @@ function readImageBlob(blob) {
799
828
  var cx = canvas.getContext("2d");
800
829
  cx.drawImage(img, 0, 0, nw, nh);
801
830
  var resized = canvas.toDataURL("image/jpeg", RESIZE_QUALITY);
802
- addPendingImage(resized);
831
+ addPendingImage(resized); finish();
803
832
  };
833
+ img.onerror = function () { finish(); showToast('Image could not be loaded. Your draft is preserved.', 'error'); };
804
834
  img.src = dataUrl;
805
835
  };
806
836
  reader.readAsDataURL(blob);
@@ -869,7 +899,9 @@ function sendInputSync() {
869
899
  if (dk) ctx.ws.send(JSON.stringify({ type: "dm_typing", dmKey: dk, typing: hasText }));
870
900
  return;
871
901
  }
872
- ctx.ws.send(JSON.stringify({ type: "input_sync", text: ctx.inputEl.value }));
902
+ var speech = store.get('speechActive');
903
+ ctx.ws.send(JSON.stringify({ type: "input_sync", text: ctx.inputEl.value,
904
+ paneBinding: speech && speech.writing ? speech.paneBinding || undefined : undefined }));
873
905
  }
874
906
 
875
907
  export function handleInputSync(text) {
@@ -893,7 +925,7 @@ function createFileInput(accept, capture, multiple) {
893
925
  input.style.display = "none";
894
926
  document.body.appendChild(input);
895
927
 
896
- input.addEventListener("change", function () {
928
+ var selectFiles = sessionCallback(function () {
897
929
  if (input.files) {
898
930
  for (var i = 0; i < input.files.length; i++) {
899
931
  if (input.files[i].type.indexOf("image/") === 0) {
@@ -903,6 +935,9 @@ function createFileInput(accept, capture, multiple) {
903
935
  }
904
936
  }
905
937
  }
938
+ });
939
+ input.addEventListener("change", function () {
940
+ selectFiles();
906
941
  document.body.removeChild(input);
907
942
  });
908
943
 
@@ -991,6 +1026,14 @@ export function initInput(_ctx) {
991
1026
  }
992
1027
  }
993
1028
 
1029
+ document.addEventListener('clay-composer-handoff', function () {
1030
+ if (scheduleInlineInput) {
1031
+ var minutes = parseInt(scheduleInlineInput.value, 10);
1032
+ if (minutes >= 1 && minutes <= 1440) setScheduleDelayMs(minutes * 60000);
1033
+ }
1034
+ collapseScheduleBtn();
1035
+ });
1036
+
994
1037
  function setScheduleDelay(ms) {
995
1038
  scheduleDelayMs = ms;
996
1039
  if (!scheduleBtn) return;
@@ -1020,7 +1063,7 @@ export function initInput(_ctx) {
1020
1063
  scheduleInlineInput.className = "schedule-inline-input";
1021
1064
  scheduleBtn.appendChild(scheduleInlineInput);
1022
1065
 
1023
- setTimeout(function () { scheduleInlineInput.focus(); }, 0);
1066
+ setTimeout(sessionCallback(function () { if (scheduleInlineInput) scheduleInlineInput.focus(); }), 0);
1024
1067
 
1025
1068
  scheduleInlineInput.addEventListener("click", function (e) { e.stopPropagation(); });
1026
1069
  scheduleInlineInput.addEventListener("keydown", function (e) {
@@ -1039,6 +1082,7 @@ export function initInput(_ctx) {
1039
1082
 
1040
1083
  // Close on outside click
1041
1084
  setTimeout(function () {
1085
+ if (!scheduleInlineInput) return;
1042
1086
  scheduleOutsideHandler = function (ev) {
1043
1087
  if (!scheduleBtn.contains(ev.target)) {
1044
1088
  if (scheduleInlineInput) {
@@ -0,0 +1,17 @@
1
+ // Decorative opening state; conversation readiness remains with its host.
2
+ export function mateLoadingMarkup() {
3
+ return '<div data-clay-loading class="mate-loading" role="status" aria-live="polite">' +
4
+ '<div class="mate-loading-scene" aria-hidden="true">' +
5
+ '<img class="mate-loading-avatar" alt="">' +
6
+ '<img class="mate-loading-desk" src="/welcome-art/conversation-desk.png" alt="" decoding="async">' +
7
+ '</div><p class="mate-loading-label"></p>' +
8
+ '<div class="mate-loading-dots" aria-hidden="true"><span></span><span></span><span></span></div></div>';
9
+ }
10
+
11
+ export function updateMateLoadingScene(element, name, avatar) {
12
+ var image = element.querySelector('.mate-loading-avatar');
13
+ if (image.getAttribute('src') !== avatar) image.setAttribute('src', avatar);
14
+ var label = element.querySelector('.mate-loading-label');
15
+ var text = 'Getting ready with ' + name;
16
+ if (label.textContent !== text) label.textContent = text;
17
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { store } from './store.js';
4
4
  import { getWs } from './ws-ref.js';
5
+ import { paneMessageBinding } from './pane-binding.js';
5
6
 
6
7
  var ACK_TIMEOUT_MS = 5000;
7
8
  var ACK_RETRY_LIMIT = 3;
@@ -152,7 +153,7 @@ function transmit(entry, automatic) {
152
153
  }
153
154
  socketRefs[entry.payload.clientMessageId] = socket;
154
155
  try {
155
- socket.send(JSON.stringify(entry.payload));
156
+ if (socket.send(JSON.stringify(entry.payload)) === false) { markUnconfirmed(entry); return false; }
156
157
  armAckTimer(entry.payload.clientMessageId, socket);
157
158
  return true;
158
159
  } catch (e) {
@@ -163,6 +164,8 @@ function transmit(entry, automatic) {
163
164
 
164
165
  export function sendAcknowledgedMessage(payload) {
165
166
  var nextPayload = Object.assign({}, payload, { clientMessageId: createClientMessageId() });
167
+ var binding = paneMessageBinding();
168
+ if (binding && !nextPayload.paneBinding) nextPayload.paneBinding = binding;
166
169
  var entry = {
167
170
  projectSlug: store.get('currentSlug'),
168
171
  sessionId: store.get('activeSessionId'),
@@ -193,7 +196,12 @@ export function sendAcknowledgedMessage(payload) {
193
196
  export function retryPendingMessage(clientMessageId) {
194
197
  var entry = pendingEntry(clientMessageId);
195
198
  if (!matchesActivatedContext(entry) || hasKnownCliMismatch(entry)) return false;
196
- var retryEntry = Object.assign({}, entry, { attempts: 0, status: "pending" });
199
+ // An explicit Retry in the same durable conversation may use its new live
200
+ // binding. Automatic retry retains the original action binding and refuses.
201
+ var payload = Object.assign({}, entry.payload);
202
+ var binding = paneMessageBinding();
203
+ if (binding) payload.paneBinding = binding;
204
+ var retryEntry = Object.assign({}, entry, { payload: payload, attempts: 0, status: "pending" });
197
205
  replacePending(pendingMessages().map(function (candidate) {
198
206
  return candidate.payload.clientMessageId === clientMessageId ? retryEntry : candidate;
199
207
  }));
@@ -50,6 +50,8 @@ export function setupModelPicker() {
50
50
 
51
51
  export function resetModelPickerState() {
52
52
  clearRequestTimer();
53
+ if (configPopover) configPopover.classList.add('hidden');
54
+ if (configChip) configChip.classList.remove('active');
53
55
  store.set({
54
56
  modelListVendor: "",
55
57
  modelListStatus: "idle",
@@ -0,0 +1,214 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { getCachedSessions } from './sidebar-sessions.js';
4
+ import { takeComposerDraft, restoreComposerDraft } from './input.js';
5
+ import { resetHistoryReplayBatch } from './history-replay-batch.js';
6
+ import { updateContextPanel } from './app-panels.js';
7
+ import { paneTarget, samePaneBinding, samePaneTarget } from './pane-binding.js';
8
+
9
+ function enabled() {
10
+ return store.get('paneMode') && !store.get('paneActivationUnsupported') && !store.get('activeProjectMateId') && !/^\/p\/mate-/.test(location.pathname) && !new URLSearchParams(location.search).has('contextual') && window.parent !== window;
11
+ }
12
+ function block(value) {
13
+ var app = document.getElementById('app');
14
+ if (app) app.inert = value;
15
+ document.documentElement.dataset.paneReady = value ? 'false' : 'true';
16
+ }
17
+ function draftKey() { return String(store.get('myUserId')) + ':' + store.get('sessionOriginId'); }
18
+ function clearDeadline() {
19
+ if (store.get('paneActivationTimer')) clearTimeout(store.get('paneActivationTimer'));
20
+ store.set({ paneActivationTimer: null });
21
+ }
22
+ function failed(requestId) {
23
+ var pending = store.get('panePendingBinding');
24
+ if (!pending || requestId !== pending.requestId) return;
25
+ clearDeadline();
26
+ store.set({ paneReady: false, panePendingBinding: null });
27
+ block(true);
28
+ var frame = window.frameElement;
29
+ if (frame) frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-activation-failed'));
30
+ }
31
+
32
+ // Claim ordinary initial pins before the legacy session-list handler runs.
33
+ // Authentication, list and group metadata can arrive in either order; the
34
+ // subscription retries when those authoritative prerequisites become ready.
35
+ export function primePaneSession() {
36
+ if (!enabled() || !window.frameElement || !window.frameElement.clayPane) return false;
37
+ var id = store.get('paneSessionId');
38
+ var session = (getCachedSessions() || []).find(function (item) { return item.id === id; });
39
+ if (!session || session.isMate || (session.runtimeMode || session.mode || 'gui') !== 'gui' || session.tuiSuspended) return false;
40
+ activatePaneSession(id);
41
+ return true;
42
+ }
43
+
44
+ export function activatePaneSession(id, force, expectedTarget) {
45
+ var state = store.snap(), ws = getWs();
46
+ if (state.authIdentityLoaded === false) return false;
47
+ if (state.isMultiUserMode && !state.myUserId) return false;
48
+ if (!enabled() || !state.connected || !ws || ws.readyState !== 1 || state.splitGroupsProjectSlug !== state.currentSlug) return false;
49
+ var target = paneTarget(state, getCachedSessions(), id);
50
+ if (!target) return false;
51
+ if (expectedTarget && !samePaneTarget(target, expectedTarget)) return false;
52
+ var sourceFrame = window.frameElement;
53
+ if (!expectedTarget && sourceFrame && Number(sourceFrame.dataset && sourceFrame.dataset.sessionId) === id && sourceFrame.dataset.sessionOriginId && sourceFrame.dataset.sessionOriginId !== target.sessionOriginId) return false;
54
+ var old = state.paneCommittedBinding, pending = state.panePendingBinding;
55
+ if (!force && !state.paneSuspended && samePaneTarget(pending || old, target)) return true;
56
+ if (!pending && state.sessionOriginId) {
57
+ var draft;
58
+ try { draft = takeComposerDraft(); } catch (error) { return false; }
59
+ var drafts = Object.assign({}, state.paneRichDrafts || {});
60
+ drafts[draftKey()] = draft;
61
+ store.set({ paneRichDrafts: drafts });
62
+ }
63
+ clearDeadline();
64
+ resetHistoryReplayBatch();
65
+ var generation = (store.get('paneActivationGeneration') || 0) + 1;
66
+ target = Object.assign({}, target, { generation: generation, requestId: 'pane-' + generation + '-' + Math.random().toString(36).slice(2, 12) });
67
+ var path = new URL(store.get('wsPath'), location.origin);
68
+ path.searchParams.set('session', String(id));
69
+ store.set({ paneActivationGeneration: generation, panePendingBinding: target, paneReady: false, paneSuspended: false,
70
+ paneSessionId: id, panePinPending: false, wsPath: path.pathname + path.search });
71
+ block(true);
72
+ store.set({ paneActivationTimer: setTimeout(function () { failed(target.requestId); }, 12000) });
73
+ ws.send(JSON.stringify({ type: 'switch_session', id: id, paneActivation: target }));
74
+ return true;
75
+ }
76
+
77
+ // Runs before replay batching, so an obsolete history_meta cannot open a batch
78
+ // that later consumes destination messages.
79
+ export function filterPaneActivation(message) {
80
+ if (!enabled()) return false;
81
+ var pending = store.get('panePendingBinding'), committed = store.get('paneCommittedBinding');
82
+ if (message.type === 'pane_activation_rejected') { failed(message.requestId); return true; }
83
+ if (!pending && !committed) return !!message.paneActivation;
84
+ if (['session_list', 'split_groups', 'session_presence', 'pong', 'account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0 && !message.paneActivation) return false;
85
+ var expected = pending || committed;
86
+ if (!samePaneBinding(expected, message.paneActivation)) return true;
87
+ if (message.type === 'pane_activation_started') return true;
88
+ if (message.type === 'pane_activation_snapshot_done') {
89
+ if (!pending) return true;
90
+ var draft = (store.get('paneRichDrafts') || {})[String(store.get('myUserId')) + ':' + expected.sessionOriginId];
91
+ restoreComposerDraft(draft || { text: '', start: 0, end: 0 });
92
+ function paint() {
93
+ if (store.get('panePendingBinding') !== expected || !store.get('connected') || store.get('paneSuspended')) return;
94
+ var current = paneTarget(store.snap(), getCachedSessions(), expected.sessionId);
95
+ var messages = document.getElementById('messages');
96
+ if (!samePaneTarget(current, expected) ||
97
+ store.get('activeSessionId') !== expected.sessionId || store.get('sessionOriginId') !== expected.sessionOriginId || store.get('replayingHistory') ||
98
+ !messages || messages.dataset.controlSessionId !== String(expected.sessionId) || messages.dataset.controlSessionOrigin !== expected.sessionOriginId) { failed(expected.requestId); return; }
99
+ clearDeadline();
100
+ store.set({ paneCommittedBinding: expected, panePendingBinding: null, paneReady: true });
101
+ block(false);
102
+ var frame = window.frameElement;
103
+ if (frame) frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-ready'));
104
+ updateContextPanel();
105
+ }
106
+ requestAnimationFrame(function () { requestAnimationFrame(paint); });
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ export function paneOutgoing(data) {
113
+ var message;
114
+ try { message = JSON.parse(data); } catch (error) { return null; }
115
+ if (['ping', 'pong', 'clay_ui_ready'].indexOf(message.type) >= 0) return data;
116
+ if (store.get('deferredSessionSwitch') && store.get('deferredSessionSwitch').draining) return null;
117
+ if (message.type === 'switch_session') {
118
+ var entry = store.get('pendingSessionEntryNavigation');
119
+ if (entry && entry.projectSlug === store.get('currentSlug') && String(entry.sessionId) !== String(message.id)) store.set({ pendingSessionEntryNavigation: null });
120
+ if (enabled() && (store.get('panePendingBinding') || store.get('paneCommittedBinding')) && !message.paneActivation) return null;
121
+ return data;
122
+ }
123
+ if (store.get('deferredSessionSwitch')) {
124
+ var source = store.get('deferredSessionSwitch');
125
+ if (source.draining) return null;
126
+ if (message.type === 'stop') return JSON.stringify(Object.assign({}, message, { sessionId: source.sessionId, sessionOriginId: source.origin }));
127
+ return null;
128
+ }
129
+ if (!enabled()) return data;
130
+ var binding = store.get('paneCommittedBinding');
131
+ if (store.get('panePendingBinding') || store.get('paneSuspended') || binding && !store.get('paneReady')) return null;
132
+ if (!binding) return ['stop', 'user_message', 'permission_response', 'ask_user_response', 'elicitation_response', 'user_dialog_response', 'input_sync'].indexOf(message.type) >= 0 ? null : data;
133
+ if (message.paneBinding && !samePaneBinding(binding, message.paneBinding)) return null;
134
+ var gesture = (store.get('paneActionEvents') || []).find(function (entry) { return entry.event.isTrusted && entry.event.eventPhase && samePaneBinding(entry.binding, binding); });
135
+ var action = store.get('paneCallbackBinding') || gesture && gesture.binding;
136
+ var read = /^(get_|list_)|(_get|_list|_options)$/.test(message.type) || message.type === 'load_more_history';
137
+ if (!message.paneBinding && !read && !samePaneBinding(action, binding)) return null;
138
+ return JSON.stringify(Object.assign({}, message, { paneBinding: message.paneBinding || action || binding }));
139
+ }
140
+
141
+ export function refreshPaneActivation() {
142
+ if (!enabled()) return;
143
+ var binding = store.get('panePendingBinding') || store.get('paneCommittedBinding');
144
+ if (!binding) return;
145
+ var current = paneTarget(store.snap(), getCachedSessions(), binding.sessionId);
146
+ if (samePaneTarget(current, binding)) return;
147
+ if (store.get('panePendingBinding')) failed(binding.requestId);
148
+ else { store.set({ paneReady: false }); block(true); }
149
+ }
150
+
151
+ export function initPaneActivation() {
152
+ if (!enabled() || !window.frameElement) return;
153
+ var frame = window.frameElement;
154
+ var latestId = Number(frame.dataset && (frame.dataset.requestedSessionId || frame.dataset.sessionId));
155
+ if (Number.isSafeInteger(latestId) && latestId > 0 && frame.dataset.projectSlug === store.get('currentSlug')) {
156
+ var latestPath = new URL(store.get('wsPath'), location.origin);
157
+ latestPath.searchParams.set('session', String(latestId));
158
+ store.set({ paneSessionId: latestId, panePinPending: true, wsPath: latestPath.pathname + latestPath.search });
159
+ }
160
+ block(true);
161
+ frame.clayPane = {
162
+ source: window, document: document,
163
+ identity: function () {
164
+ var binding = store.get('paneCommittedBinding');
165
+ return binding && { ownerId: binding.viewerId, projectSlug: binding.projectSlug, sessionId: binding.sessionId,
166
+ binding: binding, ready: store.get('connected') && store.get('paneReady') && !store.get('paneSuspended') };
167
+ },
168
+ activate: activatePaneSession,
169
+ checkDraft: function () { if (!store.get('panePendingBinding')) { var draft = takeComposerDraft(); restoreComposerDraft(draft); } },
170
+ suspend: function () { store.set({ paneSuspended: true, paneReady: false }); block(true); },
171
+ };
172
+ // A pointer that began on the previous conversation cannot become a Stop or
173
+ // approval click on the replacement conversation after hydration.
174
+ document.addEventListener('pointerdown', function () { store.set({ panePointerBinding: store.get('paneCommittedBinding') }); }, true);
175
+ document.addEventListener('click', function (event) {
176
+ if (store.get('paneSuspended') || !store.get('paneReady') && store.get('panePendingBinding') || event.detail && store.get('panePointerBinding') !== store.get('paneCommittedBinding')) {
177
+ event.preventDefault(); event.stopImmediatePropagation();
178
+ }
179
+ }, true);
180
+ ['click', 'input', 'change', 'keydown', 'keyup', 'submit', 'blur'].forEach(function (name) {
181
+ document.addEventListener(name, function (event) {
182
+ var keys = store.get('paneHeldKeys') || {};
183
+ var binding = store.get('paneCommittedBinding');
184
+ var previousKey = keys[event.code || event.key];
185
+ var staleKey = (name === 'keyup' || name === 'keydown' && event.repeat) && previousKey && !samePaneBinding(previousKey, binding);
186
+ if (name === 'keydown' && !event.repeat) keys[event.code || event.key] = binding;
187
+ if (name === 'keyup') delete keys[event.code || event.key];
188
+ if (staleKey || store.get('panePendingBinding') || store.get('paneSuspended') || !store.get('paneReady')) {
189
+ event.preventDefault(); event.stopImmediatePropagation(); return;
190
+ }
191
+ var events = (store.get('paneActionEvents') || []).filter(function (entry) { return entry.event.eventPhase; });
192
+ events.push({ event: event, binding: binding });
193
+ store.set({ paneHeldKeys: keys, paneActionEvents: events });
194
+ }, true);
195
+ });
196
+ store.subscribe(function (state, previous) {
197
+ if (state.activeSessionMode === 'tui' && previous.activeSessionMode !== 'tui') {
198
+ delete frame.clayPane; store.set({ paneActivationUnsupported: true }); block(false); return;
199
+ }
200
+ if (!state.connected && previous.connected) {
201
+ clearDeadline();
202
+ store.set({ paneReady: false, panePendingBinding: null, paneCommittedBinding: null }); block(true);
203
+ }
204
+ if (state.connected && (state.splitGroups !== previous.splitGroups || state.splitGroupsProjectSlug !== previous.splitGroupsProjectSlug || state.paneBindingSessions !== previous.paneBindingSessions || state.authIdentityLoaded !== previous.authIdentityLoaded || state.myUserId !== previous.myUserId || state.activeSessionId !== previous.activeSessionId || state.connected !== previous.connected)) {
205
+ queueMicrotask(function () {
206
+ refreshPaneActivation();
207
+ if (!store.get('panePendingBinding') && !store.get('paneSuspended')) {
208
+ if (store.get('panePinPending')) primePaneSession();
209
+ else if (store.get('activeSessionId') === store.get('paneSessionId')) activatePaneSession(store.get('paneSessionId'));
210
+ }
211
+ });
212
+ }
213
+ });
214
+ }
@@ -0,0 +1,59 @@
1
+ // Live appearance sync between the app shell and its pane iframes (split
2
+ // Driver/Worker panes and the contextual Mate window). Each iframe runs its own
3
+ // theme module and only reads the persisted preference at load, so the shell
4
+ // pushes the applied appearance down on every change and when a frame loads.
5
+ // Same-origin only, shell to child only; nothing is saved from either side.
6
+
7
+ import { store } from './store.js';
8
+ import { onThemeChange, getAppearanceSnapshot, applyExternalAppearance } from './theme.js';
9
+
10
+ var ID = /^[a-zA-Z0-9_-]{1,120}$/;
11
+
12
+ function paneFrames() {
13
+ return Array.prototype.filter.call(document.querySelectorAll('iframe'), function (frame) {
14
+ try { var url = new URL(frame.src, window.location.href); return url.origin === window.location.origin && url.searchParams.get('pane') === '1'; }
15
+ catch (error) { return false; }
16
+ });
17
+ }
18
+
19
+ function push(frame) {
20
+ var snapshot = getAppearanceSnapshot();
21
+ try { if (frame.contentWindow) frame.contentWindow.postMessage({ type: 'clay-appearance', snapshot: snapshot }, window.location.origin); } catch (error) {}
22
+ }
23
+
24
+ function isId(value) { return typeof value === 'string' && ID.test(value); }
25
+ function isIdOrNull(value) { return value === null || isId(value); }
26
+
27
+ // Exact shape only: strings or null, never coerced values.
28
+ function validSnapshot(value) {
29
+ var pref = value && typeof value === 'object' ? value.preference : null;
30
+ if (!pref || typeof pref !== 'object' || !isId(value.themeId)) return false;
31
+ if (value.userId !== null && (typeof value.userId !== 'string' || !value.userId || value.userId.length > 200)) return false;
32
+ if (typeof pref.configured !== 'boolean') return false;
33
+ if (pref.mode !== null && pref.mode !== 'light' && pref.mode !== 'dark') return false;
34
+ return isIdOrNull(pref.theme) && isIdOrNull(pref.lightTheme) && isIdOrNull(pref.darkTheme);
35
+ }
36
+
37
+ export function initPaneAppearanceBridge() {
38
+ if (store.get('paneMode')) {
39
+ if (window.parent === window) return;
40
+ window.addEventListener('message', function (event) {
41
+ if (event.origin !== window.location.origin || event.source !== window.parent) return;
42
+ var msg = event.data;
43
+ if (!msg || msg.type !== 'clay-appearance' || !validSnapshot(msg.snapshot)) return;
44
+ // Once this frame knows its owner the snapshot must name exactly that owner (null is a mismatch).
45
+ // Before identity is known, or in solo mode without one, any well-formed snapshot applies.
46
+ var owner = store.get('myUserId') || null;
47
+ if (owner !== null && msg.snapshot.userId !== owner) return;
48
+ applyExternalAppearance(msg.snapshot);
49
+ });
50
+ return;
51
+ }
52
+ onThemeChange(function () { paneFrames().forEach(push); });
53
+ // Frames that rejected an early snapshot (identity not yet known here) get it again once it is.
54
+ store.subscribe(function (state, previous) { if (state.myUserId !== previous.myUserId) paneFrames().forEach(push); });
55
+ // A frame that finished loading may have been seeded before a pending save.
56
+ document.addEventListener('load', function (event) {
57
+ if (event.target && event.target.tagName === 'IFRAME' && paneFrames().indexOf(event.target) >= 0) push(event.target);
58
+ }, true);
59
+ }
@@ -0,0 +1,82 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+
4
+ export function captureSessionAction() {
5
+ var state = store.snap();
6
+ return { projectSlug: state.currentSlug, ownerId: state.myUserId, sessionId: state.activeSessionId,
7
+ origin: state.sessionOriginId, socket: getWs(), binding: state.paneCommittedBinding || null,
8
+ epoch: state.sessionActionEpoch || 0 };
9
+ }
10
+
11
+ export function sessionActionCurrent(action, draftCompletion) {
12
+ var state = store.snap();
13
+ return !!action && action.socket === getWs() && state.connected &&
14
+ action.projectSlug === state.currentSlug && action.ownerId === state.myUserId &&
15
+ action.sessionId === state.activeSessionId && action.origin === state.sessionOriginId &&
16
+ action.epoch === (state.sessionActionEpoch || 0) &&
17
+ (draftCompletion || !state.deferredSessionSwitch) && !state.panePendingBinding && !state.paneSuspended &&
18
+ (action.binding ? state.paneReady && samePaneBinding(action.binding, state.paneCommittedBinding) : !state.paneCommittedBinding);
19
+ }
20
+
21
+ // Capture at callback creation, never when an old callback eventually sends.
22
+ export function sessionCallback(callback, draftCompletion) {
23
+ var action = captureSessionAction();
24
+ return function () {
25
+ if (!sessionActionCurrent(action, draftCompletion)) return;
26
+ var previous = store.get('paneCallbackBinding');
27
+ store.set({ paneCallbackBinding: action.binding });
28
+ try { return callback.apply(this, arguments); }
29
+ finally { store.set({ paneCallbackBinding: previous }); }
30
+ };
31
+ }
32
+
33
+ export function samePaneBinding(a, b) {
34
+ return !!a && !!b && ['requestId', 'generation', 'projectSlug', 'viewerId', 'ownerId', 'sessionId', 'sessionOriginId', 'role', 'workerGeneration', 'groupStamp', 'parentSessionOriginId'].every(function (key) { return a[key] === b[key]; });
35
+ }
36
+
37
+ export function samePaneTarget(a, b) {
38
+ return !!a && !!b && ['projectSlug', 'viewerId', 'ownerId', 'sessionId', 'sessionOriginId', 'role', 'workerGeneration', 'groupStamp', 'parentSessionOriginId'].every(function (key) { return a[key] === b[key]; });
39
+ }
40
+
41
+ export function paneTarget(state, sessions, id) {
42
+ var session = (sessions || []).find(function (item) { return item.id === id; });
43
+ if (!session || session.isMate || !session.sessionOriginId || (session.runtimeMode || session.mode || 'gui') !== 'gui' || session.tuiSuspended) return null;
44
+ var group = (state.splitGroups || []).find(function (item) { return item.members.indexOf(id) >= 0; });
45
+ var parent = session.sessionRole === 'worker' && sessions.find(function (item) { return item.id === session.parentSessionId; });
46
+ if (session.sessionRole === 'worker' && (!parent || !session.parentAvailable || !Number.isSafeInteger(session.workerGeneration) || !parent.sessionOriginId || parent.ownerId !== session.ownerId || !group || !group.pair || group.pair.driverId !== parent.id)) return null;
47
+ return {
48
+ projectSlug: state.currentSlug, viewerId: state.myUserId || null, ownerId: session.ownerId || null,
49
+ sessionId: session.id, sessionOriginId: session.sessionOriginId,
50
+ role: session.sessionRole === 'worker' ? 'worker' : 'driver', workerGeneration: session.workerGeneration || null,
51
+ parentSessionOriginId: parent ? parent.sessionOriginId : null,
52
+ groupStamp: group ? JSON.stringify({ id: group.id, members: group.members, pair: group.pair || null }) : null,
53
+ };
54
+ }
55
+
56
+ // Ordinary reusable panes have a live document binding; legacy/canonical frames
57
+ // retain their existing URL and receipt checks in their own bridge paths.
58
+ export function livePaneBinding(frame, message) {
59
+ if (!frame || !frame.clayPane) return false;
60
+ var api = frame.clayPane, live = api.identity();
61
+ var state = store.snap(), split = state.splitPanes;
62
+ var target = live && paneTarget(state, state.paneBindingSessions || [], live.sessionId);
63
+ return api.source === frame.contentWindow && api.document === frame.contentDocument && live && live.ready &&
64
+ samePaneTarget(target, live.binding) &&
65
+ live.ownerId === (state.myUserId || null) && live.projectSlug === state.currentSlug &&
66
+ split && split.panes.some(function (pane) { return pane.slug === live.projectSlug && pane.sessionId === live.sessionId; }) &&
67
+ (!message || samePaneBinding(live.binding, message.paneBinding));
68
+ }
69
+
70
+ export function paneMessageBinding() {
71
+ return store.get('paneReady') && !store.get('paneSuspended') ? store.get('paneCommittedBinding') : null;
72
+ }
73
+
74
+ export function currentPaneReview(container) {
75
+ if (!container || !container.paneBinding) return true;
76
+ return container.isConnected !== false && store.get('paneReady') && !store.get('paneSuspended') && samePaneBinding(container.paneBinding, store.get('paneCommittedBinding'));
77
+ }
78
+
79
+ export function paneCallbackCurrent(binding) {
80
+ var current = store.get('panePendingBinding') || store.get('paneCommittedBinding');
81
+ return current ? samePaneBinding(current, binding) : !binding;
82
+ }