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
@@ -238,9 +238,16 @@ function tickRateLimitUsage() {
238
238
  renderRateLimitUsage();
239
239
  }
240
240
 
241
+ // Once the server's vendor overview has answered, every vendor including the
242
+ // displayed one is shown by the overview chips, so this session-scoped link
243
+ // stands down. Rejection, warning and scheduling state is untouched.
244
+ function overviewOwnsUsageDisplay() {
245
+ return !!store.get('vendorUsageOverview');
246
+ }
247
+
241
248
  function renderRateLimitUsage() {
242
249
  var activeVendor = store.get('currentVendor') || "claude";
243
- if (!vendorTracksRateLimits(activeVendor)) {
250
+ if (!vendorTracksRateLimits(activeVendor) || overviewOwnsUsageDisplay()) {
244
251
  if (rateLimitUsageEl) {
245
252
  rateLimitUsageEl.remove();
246
253
  rateLimitUsageEl = null;
@@ -320,6 +327,10 @@ export function initRateLimit() {
320
327
  || state.currentSlug !== prev.currentSlug || state.myUserId !== prev.myUserId) {
321
328
  resetRateLimitState();
322
329
  }
330
+ if (state.vendorUsageOverview !== prev.vendorUsageOverview && state.currentVendor === prev.currentVendor && state.activeSessionId === prev.activeSessionId
331
+ && state.currentSlug === prev.currentSlug && state.myUserId === prev.myUserId) {
332
+ renderRateLimitUsage();
333
+ }
323
334
  if (state.vendorInfo !== prev.vendorInfo && state.currentVendor === prev.currentVendor && state.activeSessionId === prev.activeSessionId
324
335
  && state.currentSlug === prev.currentSlug && state.myUserId === prev.myUserId) {
325
336
  renderRateLimitUsage();
@@ -1,5 +1,4 @@
1
1
  import { observeNavigationRendering } from './clay-navigation-render.js';
2
- import { dismissContextualClay } from './contextual-clay.js';
3
2
  import { showClayNavigationActivity, clearClayControlActivity } from './clay-control-activity.js';
4
3
  import { store } from './store.js';
5
4
  import { getWs } from './ws-ref.js';
@@ -23,8 +22,6 @@ export function handleOwnerNavigation(msg) {
23
22
  var status = 'refused';
24
23
  if (accepted && msg.target && typeof msg.target.projectSlug === 'string' && Number.isSafeInteger(msg.target.sessionId)) {
25
24
  try {
26
- var contextual = store.get('contextualClay') || {};
27
- if (msg.target.contextualSourceMateId && contextual.mateId === msg.target.contextualSourceMateId && contextual.mode === 'popover') dismissContextualClay(false);
28
25
  showClayNavigationActivity(msg.target, false); navigateToSession(msg.target, null, msg.target.entryIndex); status = 'accepted';
29
26
  } catch (error) { showClayNavigationActivity(msg.target, true); status = 'failed'; }
30
27
  }
@@ -1,5 +1,6 @@
1
1
  import { forwardDocumentSource } from './clay-studio-document.js';
2
2
  import { store } from './store.js';
3
+ import { paneMessageBinding, livePaneBinding } from './pane-binding.js';
3
4
  import { getWs } from './ws-ref.js';
4
5
  import { contextualMateStamp } from './contextual-mate-identity.js';
5
6
  import { visibleTarget } from './clay-control-target-visibility.js';
@@ -34,7 +35,7 @@ export function forwardStudioPresentation(message, type) {
34
35
  if (Object.keys(receipts).length >= 100) return true;
35
36
  if (evidence) receipts[evidence.id] = { evidence: evidence, socket: getWs() };
36
37
  store.set({ clayStudioRelays: receipts });
37
- window.parent.postMessage({ type: type, message: message, studioSource: frame.clayStudioSource.identity() }, location.origin);
38
+ window.parent.postMessage({ type: type, message: message, paneBinding: paneMessageBinding(), studioSource: frame.clayStudioSource.identity() }, location.origin);
38
39
  return true;
39
40
  }
40
41
  export function trustedStudioFrame(event, data) {
@@ -45,6 +46,7 @@ export function trustedStudioFrame(event, data) {
45
46
  if (!live || !live.connected || live.ownerId !== (store.get('myUserId') || null) || live.ownerId !== expected.ownerId || live.slug !== expected.slug || live.sessionId !== expected.sessionId || live.origin !== expected.origin || live.runtime !== expected.runtime) return null;
46
47
  if (new URL(frame.src, location.href).pathname !== '/p/' + encodeURIComponent(live.slug) + '/') return null;
47
48
  if (frame.classList.contains('split-pane-frame')) {
49
+ if (frame.clayPane && !livePaneBinding(frame, data)) return null;
48
50
  var split = store.get('splitPanes');
49
51
  if (!split || !split.panes.some(function (pane) { return pane.slug === live.slug && String(pane.sessionId) === String(live.sessionId); })) return null;
50
52
  } else {
@@ -172,6 +172,7 @@ rows('clay-workspace', [
172
172
  ['inspect_mate_chat_change', 'Inspecting Mate conversation change', 'Mate conversation change inspected', 'Mate conversation'],
173
173
  ['control_mate_chat', 'Requesting Mate conversation change', 'Mate conversation request returned', 'Mate conversation', 'why', 'mate-chat'],
174
174
  ['control_schedules', 'Requesting schedule change', 'Schedule change request returned', 'Scheduling', 'why', 'schedule-control'],
175
+ ['read_vendor_usage', 'Reading vendor usage', 'Vendor usage read', 'Vendor usage', 'projectSlug'],
175
176
  ['read_project_records', 'Reading project records', 'Project records read', 'Project records', 'projectSlug'],
176
177
  ['inspect_project_record_change', 'Inspecting record change', 'Record change inspected', 'Project records', 'projectSlug'],
177
178
  ['control_project_records', 'Requesting record change', 'Record change request returned', 'Project records', 'why'],
@@ -1,5 +1,4 @@
1
1
  import { forwardContextualSessionCitation, validContextualCitation, contextualCitationLink } from './contextual-session-citations.js';
2
- import { dismissContextualClay } from './contextual-clay.js';
3
2
  import { mateAvatarUrl } from './avatar.js';
4
3
  import { escapeHtml } from './utils.js';
5
4
  import { refreshIcons } from './icons.js';
@@ -146,7 +145,6 @@ export function handleClaySessionTarget(msg) {
146
145
  navigateToLog(msg.target);
147
146
  return true;
148
147
  }
149
- if (citation) { dismissContextualClay(false); if ((store.get('contextualClay') || {}).mode !== 'closed') return true; }
150
148
  navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null, msg.target.entryIndex);
151
149
  return true;
152
150
  }
@@ -174,24 +172,36 @@ export function closeCommandPalette() {
174
172
 
175
173
  export function setPaletteVersion(version) { cachedVersion = version; }
176
174
 
177
- export function handlePaletteSessionSwitch() {
175
+ export function handlePaletteSessionSwitch(authoritative) {
178
176
  if (pendingLogNav && ctx.currentSlug && ctx.currentSlug() === pendingLogNav.projectSlug) {
179
177
  var logNav = pendingLogNav;
180
178
  pendingLogNav = null;
181
179
  openProjectLog(logNav.ref);
182
180
  }
183
- if (!pendingNav) return;
184
- var nav = pendingNav;
185
- pendingNav = null;
186
- if (ctx.currentSlug && ctx.currentSlug() === nav.slug) {
187
- navigateToSession({ projectSlug: nav.slug, sessionId: nav.sessionId }, nav.query, nav.entryIndex);
181
+ // session_list precedes the connection's authoritative session_switched.
182
+ // Wait for that identity instead of requesting a duplicate initial replay.
183
+ if (authoritative === true && pendingNav && store.get('activeSessionId')) {
184
+ var nav = pendingNav;
185
+ pendingNav = null;
186
+ if (ctx.currentSlug && ctx.currentSlug() === nav.slug) {
187
+ navigateToSession({ projectSlug: nav.slug, sessionId: nav.sessionId }, nav.query, nav.entryIndex);
188
+ }
188
189
  }
190
+ var pending = store.get('pendingSessionEntryNavigation');
191
+ if (pending && isActiveEntryNavigation(pending)) pending.awaitingTarget = false;
192
+ var highlight = store.get('sessionEntryHighlight');
193
+ if (highlight && !isCurrentEntryHighlight(highlight)) store.set({ sessionEntryHighlight: null });
189
194
  }
190
195
 
191
196
  export function handlePaletteHistoryDone() {
192
197
  var pending = store.get('pendingSessionEntryNavigation');
193
- if (!pending) return;
198
+ if (!pending) {
199
+ var highlight = store.get('sessionEntryHighlight');
200
+ if (highlight && isCurrentEntryHighlight(highlight)) jumpToSessionEntry(highlight.entryIndex, highlight);
201
+ return;
202
+ }
194
203
  if (!isActiveEntryNavigation(pending)) {
204
+ if (pending.awaitingTarget) return;
195
205
  store.set({ pendingSessionEntryNavigation: null });
196
206
  return;
197
207
  }
@@ -201,18 +211,26 @@ export function handlePaletteHistoryDone() {
201
211
  if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom, target: pending.entryIndex }));
202
212
  return;
203
213
  }
204
- jumpToSessionEntry(pending.entryIndex);
214
+ jumpToSessionEntry(pending.entryIndex, pending);
205
215
  }
206
216
 
207
217
  export function handlePaletteHistoryPrepended() {
208
218
  var pending = store.get('pendingSessionEntryNavigation');
209
219
  if (!pending) return;
210
220
  if (!isActiveEntryNavigation(pending)) {
221
+ if (pending.awaitingTarget) return;
211
222
  store.set({ pendingSessionEntryNavigation: null });
212
223
  return;
213
224
  }
214
225
  requestAnimationFrame(function () {
215
- if (store.get('pendingSessionEntryNavigation') === pending) handlePaletteHistoryDone();
226
+ if (store.get('pendingSessionEntryNavigation') !== pending) return;
227
+ // This frame was scheduled for an already active target. A later
228
+ // navigation cancels it even if its initial awaiting flag was retained.
229
+ if (!isActiveEntryNavigation(pending)) {
230
+ store.set({ pendingSessionEntryNavigation: null });
231
+ return;
232
+ }
233
+ handlePaletteHistoryDone();
216
234
  });
217
235
  }
218
236
 
@@ -220,6 +238,11 @@ function isActiveEntryNavigation(pending) {
220
238
  return String(store.get('currentSlug') || (ctx && ctx.currentSlug ? ctx.currentSlug() : "")) === String(pending.projectSlug) && String(store.get('activeSessionId')) === String(pending.sessionId);
221
239
  }
222
240
 
241
+ function isCurrentEntryHighlight(highlight) {
242
+ return highlight.expiresAt > Date.now() && isActiveEntryNavigation(highlight)
243
+ && highlight.sessionOriginId === (store.get('sessionOriginId') || null);
244
+ }
245
+
223
246
  function jumpToSessionEntry(entryIndex, pending) {
224
247
  if (pending && !isActiveEntryNavigation(pending)) return;
225
248
  var messages = document.querySelectorAll("#messages [data-history-index], #messages [data-history-indexes]");
@@ -228,17 +251,31 @@ function jumpToSessionEntry(entryIndex, pending) {
228
251
  var indexes = (messages[i].dataset.historyIndexes || messages[i].dataset.historyIndex || "").split(",");
229
252
  if (indexes.indexOf(String(entryIndex)) !== -1) { target = messages[i]; break; }
230
253
  }
231
- store.set({ pendingSessionEntryNavigation: null });
232
254
  if (!target) return;
255
+ store.set({ pendingSessionEntryNavigation: null });
233
256
  disarmStickyBottom();
234
257
  var oldTargets = document.querySelectorAll("#messages .session-entry-target");
235
258
  for (var oldIndex = 0; oldIndex < oldTargets.length; oldIndex++) oldTargets[oldIndex].classList.remove("session-entry-target");
236
259
  var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
260
+ // A repeated authoritative replay replaces the DOM after the initial jump.
261
+ // Retain only this exact origin for the original feedback lifetime.
262
+ var highlight = pending && pending.expiresAt ? pending : {
263
+ projectSlug: store.get('currentSlug'), sessionId: store.get('activeSessionId'),
264
+ sessionOriginId: store.get('sessionOriginId') || null, entryIndex: entryIndex,
265
+ expiresAt: Date.now() + (reducedMotion ? 700 : 1800),
266
+ };
267
+ store.set({ sessionEntryHighlight: highlight });
237
268
  target.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "center" });
238
269
  target.classList.remove("session-entry-target");
239
270
  void target.offsetWidth;
240
271
  target.classList.add("session-entry-target");
241
- setTimeout(function () { target.classList.remove("session-entry-target"); }, reducedMotion ? 700 : 1800);
272
+ target.sessionEntryHighlight = highlight;
273
+ setTimeout(function () {
274
+ if (target.sessionEntryHighlight !== highlight) return;
275
+ target.classList.remove("session-entry-target");
276
+ delete target.sessionEntryHighlight;
277
+ if (store.get('sessionEntryHighlight') === highlight) store.set({ sessionEntryHighlight: null });
278
+ }, Math.max(0, highlight.expiresAt - Date.now()));
242
279
  }
243
280
 
244
281
  function navigateToLog(target) {
@@ -433,8 +470,9 @@ function activateItem(entry) {
433
470
  export function navigateToSession(item, query, entryIndex) {
434
471
  closeCommandPalette();
435
472
  pendingNav = null;
473
+ store.set({ sessionEntryHighlight: null });
436
474
  var hasEntry = entryIndex !== null && entryIndex !== undefined && Number.isSafeInteger(Number(entryIndex)) && Number(entryIndex) >= 0;
437
- store.set({ pendingSessionEntryNavigation: hasEntry ? { projectSlug: item.projectSlug, sessionId: item.sessionId, entryIndex: Number(entryIndex) } : null });
475
+ store.set({ pendingSessionEntryNavigation: hasEntry ? { projectSlug: item.projectSlug, sessionId: item.sessionId, entryIndex: Number(entryIndex), awaitingTarget: true } : null });
438
476
  if (ctx.currentSlug && ctx.currentSlug() === item.projectSlug) {
439
477
  var alreadyActive = String(store.get('activeSessionId')) === String(item.sessionId);
440
478
  if (alreadyActive) {
@@ -89,7 +89,9 @@ export function finishContextualWorkspace() {
89
89
  if (handoff.stage === 'workspace' && store.get('currentSlug') === handoff.slug && store.get('activeSessionId') === handoff.sessionId) draft = takeComposerDraft();
90
90
  connection.restore(draft);
91
91
  connection.suspend(false);
92
- store.set({ contextualWorkspace: Object.assign({}, handoff, { draft: null, stage: 'returning' }), contextualClay: Object.assign({}, store.get('contextualClay'), { mode: 'closed' }) });
92
+ var presentation = store.get('contextualClay') || {};
93
+ var keepOverlay = presentation.mode === 'popover' && connection !== bridge();
94
+ store.set({ contextualWorkspace: Object.assign({}, handoff, { draft: null, stage: 'returning' }), contextualClay: keepOverlay ? presentation : Object.assign({}, presentation, { mode: 'closed' }) });
93
95
  lockNative(true);
94
96
  return true;
95
97
  } catch (error) { showToast(error.message, 'error'); return false; }
@@ -136,7 +138,17 @@ export function renderContextualWorkspaceBack() {
136
138
 
137
139
  export function prepareContextualWorkspaceDeparture(slug) {
138
140
  var origin = store.get('contextualNativeOrigin');
139
- if (origin && slug !== origin.slug && !finishContextualNativeOrigin()) return false;
141
+ if (origin && slug !== origin.slug) {
142
+ if ((store.get('contextualClay') || {}).mode === 'popover') {
143
+ var connection = bridge(origin.mateId, origin.sessionId);
144
+ try {
145
+ if (origin.stage !== 'pane' || !connection || !connection.identity().ready) throw new Error('Wait for this Mate conversation to reconnect.');
146
+ // The pane keeps its composer when its former native parent changes.
147
+ connection.checkDraft();
148
+ store.set({ contextualNativeOrigin: null }); lockNative(false);
149
+ } catch (error) { showToast(error.message, 'error'); return false; }
150
+ } else if (!finishContextualNativeOrigin()) return false;
151
+ }
140
152
  var handoff = store.get('contextualWorkspace');
141
153
  if (handoff && handoff.stage.indexOf('workspace') === 0 && slug !== handoff.slug) return finishContextualWorkspace();
142
154
  }
@@ -1,3 +1,4 @@
1
+ import { mateLoadingMarkup, updateMateLoadingScene } from './mate-loading-scene.js';
1
2
  import { attachContextualSessionCitations } from './contextual-session-citations.js';
2
3
  import { attachContextualConversations } from './contextual-mate-sessions.js';
3
4
  import { getCachedSessions } from './sidebar-sessions.js';
@@ -34,6 +35,8 @@ function escapePopover(event) {
34
35
  if (event.defaultPrevented || contextualClayMode() !== 'popover') return;
35
36
  var context = host().querySelector('details[open]');
36
37
  if (context) { context.open = false; context.querySelector('summary').focus(); return; }
38
+ var focused = document.activeElement;
39
+ if (!host().contains(focused) && focused !== trigger()) return;
37
40
  event.preventDefault(); dismissContextualClay(true);
38
41
  });
39
42
  }
@@ -65,6 +68,7 @@ function render() {
65
68
  var mate = contextualMate(state().mateId);
66
69
  if (mate) {
67
70
  var name = getHomeMateName(mate);
71
+ updateMateLoadingScene(surface.querySelector('[data-clay-loading]'), name, mateAvatarUrl(mate, 96));
68
72
  surface.setAttribute('aria-label', name + ' conversation');
69
73
  surface.querySelector('strong').textContent = name;
70
74
  var header = surface.querySelector('header');
@@ -82,7 +86,16 @@ function render() {
82
86
  }
83
87
  var projectContext = (store.get('projectsHubList') || []).find(function (item) { return item.slug === store.get('currentSlug'); });
84
88
  var contextSession = getCachedSessions().find(function (item) { return item.id === store.get('activeSessionId'); });
85
- updateMateContext(surface, mate, contextualFrame(), { projectTitle: store.get('homeShellVisible') ? 'Home' : projectContext && (projectContext.title || projectContext.project || projectContext.slug), conversationTitle: contextSession && contextSession.title, projectSlug: store.get('homeShellVisible') ? null : store.get('currentSlug'), sessionId: store.get('homeShellVisible') ? null : store.get('activeSessionId'), screen: store.get('homeShellVisible') ? 'Home' : store.get('dmMode') ? 'Direct message' : 'Project', ownerId: store.get('myUserId') || null });
89
+ var screen = store.get('homeShellVisible') || store.get('projectsHubVisible') ? 'Home' : store.get('dmMode') ? 'Direct message' : 'Project';
90
+ var projectView = screen === 'Project';
91
+ var parentReady = store.get('activeProjectSlug') === store.get('currentSlug') && store.get('sessionActivatedProjectSlug') === store.get('currentSlug') && !store.get('replayingHistory');
92
+ updateMateContext(surface, mate, contextualFrame(), {
93
+ projectTitle: projectView ? projectContext && (projectContext.title || projectContext.project || projectContext.slug) : store.get('projectsHubVisible') ? 'Projects' : screen,
94
+ conversationTitle: projectView && parentReady && contextSession ? contextSession.title : null,
95
+ projectSlug: projectView && parentReady ? store.get('currentSlug') : null,
96
+ sessionId: projectView && parentReady ? store.get('activeSessionId') : null,
97
+ screen: screen, ownerId: store.get('myUserId') || null
98
+ });
86
99
  if (trigger()) trigger().setAttribute('aria-expanded', String(mode === 'popover'));
87
100
  var selectedFrame = contextualFrame();
88
101
  var connection = selectedFrame && selectedFrame.conversation;
@@ -119,7 +132,7 @@ function ensureSurface() {
119
132
  if (!host()) {
120
133
  var surface = document.createElement('section');
121
134
  surface.id = 'contextual-clay'; surface.setAttribute('role', 'dialog');
122
- surface.innerHTML = '<header><img class="contextual-clay-avatar" alt=""><div class="contextual-clay-identity"><strong></strong><div class="contextual-context-line"><span data-clay-context></span><details class="contextual-clay-context"><summary aria-label="What this Mate can see" title="What this Mate can see">' + iconHtml('circle-help') + '</summary><p></p></details></div></div><div class="contextual-clay-actions"><button type="button" data-clay-expand>' + iconHtml('arrow-up-right') + '<span></span></button><button type="button" data-clay-close title="Close">' + iconHtml('x') + '</button></div></header><p class="contextual-capabilities" role="note" aria-label="Conversation capabilities"></p><div class="contextual-clay-body"><p data-clay-loading role="status">Opening conversation…</p></div>';
135
+ surface.innerHTML = '<header><img class="contextual-clay-avatar" alt=""><div class="contextual-clay-identity"><strong></strong><div class="contextual-context-line"><span data-clay-context></span><details class="contextual-clay-context"><summary aria-label="What this Mate can see" title="What this Mate can see">' + iconHtml('circle-help') + '</summary><p></p></details></div></div><div class="contextual-clay-actions"><button type="button" data-clay-expand>' + iconHtml('arrow-up-right') + '<span></span></button><button type="button" data-clay-close title="Close">' + iconHtml('x') + '</button></div></header><p class="contextual-capabilities" role="note" aria-label="Conversation capabilities"></p><div class="contextual-clay-body">' + mateLoadingMarkup() + '</div>';
123
136
  surface.addEventListener('contextual-clay-keydown', function (event) {
124
137
  var detail = event.detail; var frame = contextualFrame();
125
138
  if (!detail || !frame || detail.source !== frame.contentWindow) return;
@@ -193,7 +206,7 @@ export function initContextualClay() {
193
206
  initClayControlActivity();
194
207
  initContextualWorkspace();
195
208
  document.addEventListener('pointerdown', function (event) {
196
- if (contextualClayMode() === 'popover' && host() && !host().contains(event.target) && !(trigger() && trigger().contains(event.target)) && !event.target.closest('#contextual-mate-hint, .icon-strip-project-mate, .icon-strip-brand')) dismissContextualClay(false);
209
+ if (host() && !host().contains(event.target)) host().querySelectorAll('details[open]').forEach(function (menu) { menu.open = false; });
197
210
  });
198
211
  document.addEventListener('keydown', function (event) {
199
212
  if (event.key === 'Escape' && !event.defaultPrevented && contextualClayMode() === 'popover') {
@@ -231,7 +244,9 @@ export function initContextualClay() {
231
244
  var pending = value.contextualMatePending;
232
245
  store.set({ contextualMatePending: null }); activateContextualMate(pending); return;
233
246
  }
234
- if (current.mode === 'popover' && (value.currentSlug !== previous.currentSlug || value.homeShellVisible !== previous.homeShellVisible || value.dmMode !== previous.dmMode)) dismissContextualClay(false);
247
+ // Ordinary navigation refreshes context without replacing the conversation.
248
+ // Direct navigation into a native Mate workspace ends the floating surface.
249
+ if (current.mode === 'popover' && value.currentSlug !== previous.currentSlug && (value.currentSlug || '').indexOf('mate-') === 0) dismissContextualClay(false);
235
250
  render();
236
251
  });
237
252
  }
@@ -1,12 +1,13 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
+ import { contextualFrame } from './contextual-mate-identity.js';
3
4
 
4
5
  function identity(frame) {
5
6
  try { return frame && frame.conversation && frame.conversation.identity(); } catch (error) { return null; }
6
7
  }
7
8
  function contextKey(frame, canonical) {
8
9
  var current = identity(frame);
9
- return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('homeShellVisible'), store.get('dmMode'), store.get('rightWorkbenchOwner'), store.get('homeSubSurface'), store.get('rightWorkbenchRevision'), store.get('myUserId'), store.get('connected'), current && current.mateId, current && current.sessionId, current && (canonical ? current.connected : current.ready), canonical && current && current.sessionOriginId]);
10
+ return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('sessionOriginId'), store.get('activeProjectSlug'), store.get('sessionActivatedProjectSlug'), store.get('replayingHistory'), (store.get('contextualClay') || {}).mode, store.get('homeShellVisible'), store.get('projectsHubVisible'), store.get('dmMode'), store.get('rightWorkbenchOwner'), store.get('homeSubSurface'), store.get('rightWorkbenchRevision'), store.get('myUserId'), store.get('connected'), current && current.mateId, current && current.sessionId, current && (canonical ? current.connected : current.ready), canonical && current && current.sessionOriginId]);
10
11
  }
11
12
  export function updateMateContext(surface, mate, frame, snapshot) {
12
13
  var current = identity(frame), ws = getWs(), key = contextKey(frame, mate && mate.builtinKey === 'clay'), prepared = store.get('contextualPrepared');
@@ -16,11 +17,12 @@ export function updateMateContext(surface, mate, frame, snapshot) {
16
17
  capabilities.setAttribute('aria-label', 'Clay capabilities');
17
18
  capabilities.textContent = "I can control Clay Studio for you—manage projects, coordinate work, set up schedules, and navigate your workspace. Just tell me what you’d like done.";
18
19
  var privateWorkspace = (store.get('currentSlug') || '').indexOf('mate-') === 0;
19
- var available = !canonical && !privateWorkspace && current && current.ready && store.get('connected') && !store.get('homeShellVisible') && !store.get('dmMode') && !store.get('rightWorkbenchOwner') && ws && ws.readyState === 1;
20
+ var parentReady = store.get('activeProjectSlug') === store.get('currentSlug') && store.get('sessionActivatedProjectSlug') === store.get('currentSlug') && !store.get('replayingHistory');
21
+ var available = (store.get('contextualClay') || {}).mode === 'popover' && parentReady && !canonical && !privateWorkspace && current && current.ready && store.get('connected') && !store.get('homeShellVisible') && !store.get('projectsHubVisible') && !store.get('dmMode') && !store.get('rightWorkbenchOwner') && ws && ws.readyState === 1;
20
22
  var changed = !prepared || prepared.key !== key || prepared.socket !== ws;
21
23
  if (changed) {
22
24
  var requestId = 'context-' + crypto.randomUUID();
23
- prepared = { key: key, requestId: requestId, socket: ws, mateId: current && current.mateId, status: available ? 'preparing' : 'unavailable' };
25
+ prepared = { key: key, requestId: requestId, socket: ws, mateId: current && current.mateId, canonical: !!canonical, status: available ? 'preparing' : 'unavailable' };
24
26
  }
25
27
  // Stamp the reference before publishing a new preparation: store subscribers
26
28
  // may render synchronously, and the composer must never see the prior view ID.
@@ -65,7 +67,7 @@ export function updateMateContext(surface, mate, frame, snapshot) {
65
67
  export function handleMateContextMessage(msg) {
66
68
  if (msg.type === 'mate_context_prepared') {
67
69
  var pending = store.get('contextualPrepared');
68
- if (pending && pending.requestId === msg.requestId && pending.socket === getWs()) {
70
+ if (pending && pending.requestId === msg.requestId && pending.socket === getWs() && pending.key === contextKey(contextualFrame(), pending.canonical)) {
69
71
  store.set({ contextualPrepared: Object.assign({}, pending, msg, { status: msg.contextRef ? 'ready' : 'unavailable' }) });
70
72
  if (msg.contextRef) setTimeout(function () {
71
73
  var current = store.get('contextualPrepared');
@@ -4,6 +4,8 @@
4
4
  import { escapeHtml } from './utils.js';
5
5
  import { iconHtml } from './icons.js';
6
6
  import { getWs } from './ws-ref.js';
7
+ import { store } from './store.js';
8
+ import { currentPaneReview } from './pane-binding.js';
7
9
  import { fieldInfo, patternIsBounded, validateForm } from './elicitation-schema.js';
8
10
 
9
11
  var UNSET = "__clay_unset__";
@@ -154,12 +156,14 @@ function showFormError(container, message) {
154
156
  }
155
157
 
156
158
  function send(container, action, content, onSent) {
159
+ if (!currentPaneReview(container)) return false;
157
160
  var ws = getWs();
158
161
  if (!ws || ws.readyState !== 1) {
159
162
  showFormError(container, "Not connected. Try again when the connection is restored.");
160
163
  return false;
161
164
  }
162
165
  var msg = { type: "elicitation_response", requestId: container.dataset.requestId, action: action };
166
+ if (container.paneBinding) msg.paneBinding = container.paneBinding;
163
167
  if (action === "accept") msg.content = content;
164
168
  ws.send(JSON.stringify(msg));
165
169
  setBusy(container, true);
@@ -177,6 +181,7 @@ function actionButton(label, className, handler) {
177
181
 
178
182
  export function buildElicitationCard(msg, onSent) {
179
183
  var container = el("div", "permission-container elicitation-container");
184
+ container.paneBinding = store.get('panePendingBinding') || store.get('paneCommittedBinding') || null;
180
185
  container.dataset.requestId = msg.requestId;
181
186
  var header = el("div", "permission-header");
182
187
  header.innerHTML = '<span class="permission-icon">' + iconHtml("key") + '</span>' +
@@ -0,0 +1,66 @@
1
+ // github-star.js - Public star count beside the top-bar GitHub Star link.
2
+ //
3
+ // The link itself is static markup. This module only fills in the count read
4
+ // from the server's cached GET /api/github-stars. An unknown count leaves the
5
+ // count segment hidden; nothing is invented, and the module never writes to
6
+ // GitHub or sends anything about the user.
7
+
8
+ import { store } from './store.js';
9
+
10
+ var REFRESH_MS = 60 * 60 * 1000;
11
+ var MOBILE_QUERY = "(max-width: 768px)";
12
+
13
+ var timer = null;
14
+ var inflight = false;
15
+
16
+ // 999 -> "999", 1234 -> "1.2k", 12500 -> "12.5k", 1200000 -> "1.2m"
17
+ export function formatStarCount(n) {
18
+ if (typeof n !== "number" || !isFinite(n) || n < 0) return "";
19
+ n = Math.floor(n);
20
+ if (n < 1000) return String(n);
21
+ var units = [[1000000, "m"], [1000, "k"]];
22
+ for (var i = 0; i < units.length; i++) {
23
+ if (n >= units[i][0]) {
24
+ var v = Math.floor(n / units[i][0] * 10) / 10;
25
+ return (v % 1 === 0 ? String(v) : v.toFixed(1)) + units[i][1];
26
+ }
27
+ }
28
+ return String(n);
29
+ }
30
+
31
+ function render() {
32
+ var count = store.get("githubStarCount");
33
+ var countEl = document.getElementById("github-star-count");
34
+ var linkEl = document.getElementById("github-star-link");
35
+ if (!countEl || !linkEl) return;
36
+ var text = typeof count === "number" ? formatStarCount(count) : "";
37
+ countEl.textContent = text;
38
+ countEl.classList.toggle("hidden", !text);
39
+ linkEl.setAttribute("aria-label", text ? "Star Clay on GitHub, " + count + " stars" : "Star Clay on GitHub");
40
+ }
41
+
42
+ function isMobile() {
43
+ return !!(window.matchMedia && window.matchMedia(MOBILE_QUERY).matches);
44
+ }
45
+
46
+ function load() {
47
+ if (inflight || isMobile()) return Promise.resolve();
48
+ inflight = true;
49
+ return fetch("/api/github-stars", { credentials: "same-origin" }).then(function (res) {
50
+ return res.ok ? res.json() : null;
51
+ }).then(function (data) {
52
+ // Keep the last known count when the server has none or the call failed.
53
+ if (data && typeof data.stars === "number") {
54
+ store.set({ githubStarCount: data.stars });
55
+ render();
56
+ }
57
+ }).catch(function () {}).then(function () { inflight = false; });
58
+ }
59
+
60
+ export function initGithubStar() {
61
+ if (!document.getElementById("github-star")) return;
62
+ render();
63
+ load();
64
+ if (timer) clearInterval(timer);
65
+ timer = setInterval(load, REFRESH_MS);
66
+ }
@@ -27,6 +27,14 @@ function canCombine(previous, message) {
27
27
  // can differ; the first token still supplies the message's display time.
28
28
  var keys = Object.keys(previous).concat(Object.keys(message));
29
29
  return keys.every(function (key) {
30
+ // Socket JSON gives each event its own activation object. Compare every
31
+ // field, including future metadata, without weakening replay boundaries.
32
+ if (key === 'paneActivation' && previous[key] && message[key]) {
33
+ var before = previous[key], after = message[key];
34
+ return Object.keys(before).concat(Object.keys(after)).every(function (field) {
35
+ return before[field] === after[field];
36
+ });
37
+ }
30
38
  return key === 'text' || key === '_ts' || key === '_historyIndex' || key === '_historyIndexes' || previous[key] === message[key];
31
39
  });
32
40
  }
@@ -11,7 +11,6 @@ function explicitProjectPath(pathname) {
11
11
  export function resolveHomeBootDestination(options) {
12
12
  if (!options) return "wait";
13
13
  if (options.paneMode || explicitProjectPath(options.pathname)) return "project";
14
- if (/^\/projects\/?$/.test(options.pathname || "")) return "projects";
15
14
  // The server-selected project embedded in the root shell is authoritative.
16
15
  // Home is entered only through an explicit in-app action; reloading `/`
17
16
  // returns to the project workspace even if Home was the last open surface.