clay-server 5.2.0-beta.7 → 5.2.0-beta.9

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 (49) hide show
  1. package/lib/pane-pending-interactions.js +20 -0
  2. package/lib/pane-session-activation.js +114 -0
  3. package/lib/pane-session-transport.js +26 -0
  4. package/lib/project-sessions.js +15 -5
  5. package/lib/project.js +11 -3
  6. package/lib/public/app.js +14 -0
  7. package/lib/public/css/contextual-clay.css +1 -1
  8. package/lib/public/css/input.css +6 -0
  9. package/lib/public/css/sticky-notes.css +37 -11
  10. package/lib/public/modules/app-connection.js +3 -0
  11. package/lib/public/modules/app-message-router.js +13 -0
  12. package/lib/public/modules/app-messages.js +25 -5
  13. package/lib/public/modules/app-misc.js +3 -1
  14. package/lib/public/modules/app-projects.js +5 -0
  15. package/lib/public/modules/clay-owner-navigation.js +0 -3
  16. package/lib/public/modules/clay-studio-render.js +3 -1
  17. package/lib/public/modules/command-palette.js +52 -14
  18. package/lib/public/modules/contextual-clay-workspace.js +14 -2
  19. package/lib/public/modules/contextual-clay.js +16 -3
  20. package/lib/public/modules/contextual-mate-context.js +6 -4
  21. package/lib/public/modules/elicitation-form.js +5 -0
  22. package/lib/public/modules/history-replay-batch.js +8 -0
  23. package/lib/public/modules/input.js +55 -11
  24. package/lib/public/modules/message-delivery.js +10 -2
  25. package/lib/public/modules/model-picker.js +2 -0
  26. package/lib/public/modules/pane-activation.js +214 -0
  27. package/lib/public/modules/pane-binding.js +82 -0
  28. package/lib/public/modules/pane-bridge.js +5 -2
  29. package/lib/public/modules/pane-browser-bridge.js +5 -3
  30. package/lib/public/modules/pane-file-bridge.js +4 -0
  31. package/lib/public/modules/pane-issue-bridge.js +4 -1
  32. package/lib/public/modules/rewind.js +5 -0
  33. package/lib/public/modules/session-actions.js +5 -2
  34. package/lib/public/modules/session-context-menu.js +5 -0
  35. package/lib/public/modules/session-rich-drafts.js +21 -0
  36. package/lib/public/modules/session-switch-defer.js +67 -0
  37. package/lib/public/modules/split-group-restore.js +68 -0
  38. package/lib/public/modules/split-pair-ui.js +1 -0
  39. package/lib/public/modules/split-pane-reconciler.js +15 -0
  40. package/lib/public/modules/split-pane-renderer.js +87 -6
  41. package/lib/public/modules/split-session-boundary.js +1 -1
  42. package/lib/public/modules/split-view.js +51 -52
  43. package/lib/public/modules/sticky-notes-checklist.js +51 -6
  44. package/lib/public/modules/stt-controller.js +4 -1
  45. package/lib/public/modules/stt-recording.js +1 -1
  46. package/lib/public/modules/stt.js +7 -2
  47. package/lib/public/modules/tools.js +18 -2
  48. package/lib/sessions.js +17 -10
  49. package/package.json +4 -3
@@ -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
  }
@@ -34,6 +34,8 @@ function escapePopover(event) {
34
34
  if (event.defaultPrevented || contextualClayMode() !== 'popover') return;
35
35
  var context = host().querySelector('details[open]');
36
36
  if (context) { context.open = false; context.querySelector('summary').focus(); return; }
37
+ var focused = document.activeElement;
38
+ if (!host().contains(focused) && focused !== trigger()) return;
37
39
  event.preventDefault(); dismissContextualClay(true);
38
40
  });
39
41
  }
@@ -82,7 +84,16 @@ function render() {
82
84
  }
83
85
  var projectContext = (store.get('projectsHubList') || []).find(function (item) { return item.slug === store.get('currentSlug'); });
84
86
  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 });
87
+ var screen = store.get('homeShellVisible') || store.get('projectsHubVisible') ? 'Home' : store.get('dmMode') ? 'Direct message' : 'Project';
88
+ var projectView = screen === 'Project';
89
+ var parentReady = store.get('activeProjectSlug') === store.get('currentSlug') && store.get('sessionActivatedProjectSlug') === store.get('currentSlug') && !store.get('replayingHistory');
90
+ updateMateContext(surface, mate, contextualFrame(), {
91
+ projectTitle: projectView ? projectContext && (projectContext.title || projectContext.project || projectContext.slug) : store.get('projectsHubVisible') ? 'Projects' : screen,
92
+ conversationTitle: projectView && parentReady && contextSession ? contextSession.title : null,
93
+ projectSlug: projectView && parentReady ? store.get('currentSlug') : null,
94
+ sessionId: projectView && parentReady ? store.get('activeSessionId') : null,
95
+ screen: screen, ownerId: store.get('myUserId') || null
96
+ });
86
97
  if (trigger()) trigger().setAttribute('aria-expanded', String(mode === 'popover'));
87
98
  var selectedFrame = contextualFrame();
88
99
  var connection = selectedFrame && selectedFrame.conversation;
@@ -193,7 +204,7 @@ export function initContextualClay() {
193
204
  initClayControlActivity();
194
205
  initContextualWorkspace();
195
206
  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);
207
+ if (host() && !host().contains(event.target)) host().querySelectorAll('details[open]').forEach(function (menu) { menu.open = false; });
197
208
  });
198
209
  document.addEventListener('keydown', function (event) {
199
210
  if (event.key === 'Escape' && !event.defaultPrevented && contextualClayMode() === 'popover') {
@@ -231,7 +242,9 @@ export function initContextualClay() {
231
242
  var pending = value.contextualMatePending;
232
243
  store.set({ contextualMatePending: null }); activateContextualMate(pending); return;
233
244
  }
234
- if (current.mode === 'popover' && (value.currentSlug !== previous.currentSlug || value.homeShellVisible !== previous.homeShellVisible || value.dmMode !== previous.dmMode)) dismissContextualClay(false);
245
+ // Ordinary navigation refreshes context without replacing the conversation.
246
+ // Direct navigation into a native Mate workspace ends the floating surface.
247
+ if (current.mode === 'popover' && value.currentSlug !== previous.currentSlug && (value.currentSlug || '').indexOf('mate-') === 0) dismissContextualClay(false);
235
248
  render();
236
249
  });
237
250
  }
@@ -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>' +
@@ -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
  }
@@ -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) {
@@ -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",