clay-server 5.3.0-beta.2 → 5.3.0-beta.4

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.
@@ -404,11 +404,12 @@ body.split-view-resizing .split-pane-frame { pointer-events: none; }
404
404
  .split-view-worker-popover-header strong { flex: 1; min-width: 0; font-size: 12px; }
405
405
  .split-view-worker-popover-source { padding: 0 13px 10px; color: var(--text-dimmer); font-size: 10px; }
406
406
  .split-view-worker-popover-body { flex: 1; min-height: 0; overflow: auto; padding: 14px; }
407
- .split-view-worker-popover-body .message { display: block; margin: 0 0 18px; padding: 0; max-width: none; }
407
+ .split-view-worker-popover-body .message { display: flex; flex-direction: column; align-items: stretch; margin: 0 0 18px; padding: 0; max-width: none; }
408
408
  .split-view-worker-popover-body .author { display: block; margin-bottom: 4px; font-size: 11px; color: var(--text-muted); }
409
409
  .split-view-worker-popover-body .message-body { white-space: normal; overflow-wrap: anywhere; font-size: 12px; line-height: 1.65; }
410
410
  .split-view-worker-popover-body .message-body pre { white-space: pre-wrap; }
411
- .split-view-worker-popover-body .worker-tool-message summary { cursor: pointer; font-size: 11px; }
411
+ .split-view-worker-popover-body .worker-tool-message pre { max-height: 280px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
412
+ .split-view-worker-popover-body .worker-tool-message summary { cursor: pointer; font-size: 13px; }
412
413
  .split-view-worker-popover-body .worker-tool-message { padding: 6px 8px; border-radius: 5px; background: var(--bg-alt); }
413
414
  .split-view-worker-popover-footer { padding: 8px 13px; border-top: 1px solid var(--border-subtle); color: var(--text-dimmer); font-size: 10px; }
414
415
  .split-view-worker-page-status { display: block; margin-top: 3px; line-height: 1.4; }
@@ -166,7 +166,7 @@ export function addUserMessage(text, images, pastes, fromUserId, fromUserName, d
166
166
 
167
167
  addToMessages(div);
168
168
  refreshIcons();
169
- forceScrollToBottom();
169
+ forceScrollToBottom(div);
170
170
  }
171
171
 
172
172
  export function addSystemMessage(text, isError) {
@@ -96,7 +96,7 @@ import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerati
96
96
  import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
97
97
 
98
98
  // --- App module imports ---
99
- import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, flushStreamBuffer, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
99
+ import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, flushStreamBuffer, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, releaseTurnAnchor, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
100
100
  import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
101
101
  import { setStatus } from './app-connection.js';
102
102
  import { resolveSwitchedVendor } from './pane-session.js';
@@ -1498,6 +1498,7 @@ export function processMessage(msg) {
1498
1498
 
1499
1499
  case "done":
1500
1500
  store.set({ turnActivity: null });
1501
+ releaseTurnAnchor();
1501
1502
  removeMatePreThinking();
1502
1503
  setActivity(null);
1503
1504
  finishThinkingTurn();
@@ -24,7 +24,7 @@ import { hideMemory } from './mate-memory.js';
24
24
  import { isSchedulerOpen, closeScheduler, resetScheduler } from './scheduler.js';
25
25
  import { closeScheduledTasks } from './scheduled-tasks.js';
26
26
  import { connect, cancelReconnect, setStatus } from './app-connection.js';
27
- import { setTurnCounter, setPrependAnchor, setActivityEl, setIsUserScrolledUp, hideSuggestionChips } from './app-rendering.js';
27
+ import { setTurnCounter, setPrependAnchor, setActivityEl, setIsUserScrolledUp, releaseTurnAnchor, hideSuggestionChips } from './app-rendering.js';
28
28
  import { resetToolState, enableMainInput, resetTurnMetaCost } from './tools.js';
29
29
  import { resetShellCommand } from './shell-command.js';
30
30
  import { clearPendingImages } from './input.js';
@@ -210,6 +210,7 @@ export function resetClientState() {
210
210
  setPrependAnchor(null);
211
211
  store.set({ loadingMore: false });
212
212
  setIsUserScrolledUp(false);
213
+ releaseTurnAnchor();
213
214
  document.getElementById("new-msg-btn").classList.add("hidden");
214
215
  setRewindMode(false);
215
216
  setActivity(null);
@@ -8,10 +8,10 @@ import { refreshIcons } from './icons.js';
8
8
  import { closeToolGroup } from './tools.js';
9
9
  import { sendMessage, hasSendableContent } from './input.js';
10
10
  import { getChatLayout } from './theme.js';
11
- import { addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
11
+ import { addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, releaseTurnAnchor, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
12
12
  import { resolveProjectAssistantIdentity, applyProjectAssistantIdentity } from './project-chat-identity.js';
13
13
  export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
14
- export { initRendering, addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, getPrependAnchor, setPrependAnchor, getActivityEl, setActivityEl, getIsUserScrolledUp, setIsUserScrolledUp, getStickyBottom, armStickyBottom, disarmStickyBottom, VENDOR_AVATARS, VENDOR_NAMES, VENDOR_HOMEPAGES } from './chat-render-runtime.js';
14
+ export { initRendering, addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, releaseTurnAnchor, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, getPrependAnchor, setPrependAnchor, getActivityEl, setActivityEl, getIsUserScrolledUp, setIsUserScrolledUp, getStickyBottom, armStickyBottom, disarmStickyBottom, VENDOR_AVATARS, VENDOR_NAMES, VENDOR_HOMEPAGES } from './chat-render-runtime.js';
15
15
  export { addUserMessage, addSystemMessage, addConflictMessage, addContextOverflowMessage } from './app-message-cards.js';
16
16
 
17
17
  // --- Module-owned state (not in store) ---
@@ -56,15 +56,25 @@ var stickyBottom = false;
56
56
  var stickyBottomQuietTimer = null;
57
57
  var stickyBottomCeilingTimer = null;
58
58
  var stickyBottomQuietMs = 750;
59
+ var TURN_ANCHOR_MARGIN = 8;
59
60
  var stickyBottomCeilingMs = 8000;
60
61
  var stickyBottomResizeObs = null;
61
62
  var stickyBottomInputBound = false;
63
+ // The sent user turn that the transcript aligns to while its reply arrives.
64
+ var turnAnchor = null;
65
+ // Completion keeps the clamp until the follow settles, so a final render or a
66
+ // 'done' inside the follow window cannot jump past the sent turn.
67
+ var turnAnchorEnded = false;
62
68
 
63
69
  function layoutScrollLock() { return store.get('paneLayoutScrollLock'); }
64
70
 
65
71
  export function beginPaneLayoutScrollLock(token, scrollTop) {
66
72
  var messagesEl = getMessagesEl();
67
73
  if (!messagesEl || !token) return false;
74
+ // A layout-only change must not undo a send that is still being followed:
75
+ // the stored reading position predates that explicit new turn, and the
76
+ // matching end receipt may never arrive. The follow keeps the turn visible.
77
+ if (stickyBottom || activeTurnAnchor(messagesEl)) return false;
68
78
  disarmStickyBottom();
69
79
  var userPosition = store.get('paneLastUserScrollTop');
70
80
  var position = Number.isFinite(userPosition) ? userPosition : Number.isFinite(scrollTop) ? Math.max(0, scrollTop) : messagesEl.scrollTop;
@@ -88,10 +98,29 @@ export function endPaneLayoutScrollLock(token) {
88
98
 
89
99
  export function getStickyBottom() { return stickyBottom; }
90
100
 
101
+ function activeTurnAnchor(messagesEl) {
102
+ if (turnAnchor && (!messagesEl || turnAnchor.parentNode !== messagesEl)) { turnAnchor = null; turnAnchorEnded = false; }
103
+ return turnAnchor;
104
+ }
105
+
106
+ // Follow the tail, but never past the top of the sent turn: a long immediate
107
+ // reply grows below the user's message instead of pushing it out of view.
108
+ function followTarget(messagesEl) {
109
+ var anchor = activeTurnAnchor(messagesEl);
110
+ if (!anchor) return messagesEl.scrollHeight;
111
+ var top = anchor.getBoundingClientRect().top - messagesEl.getBoundingClientRect().top + messagesEl.scrollTop - TURN_ANCHOR_MARGIN;
112
+ return Math.min(messagesEl.scrollHeight, Math.max(0, top));
113
+ }
114
+
115
+ export function releaseTurnAnchor() {
116
+ if (stickyBottom && turnAnchor) turnAnchorEnded = true;
117
+ else { turnAnchor = null; turnAnchorEnded = false; }
118
+ }
119
+
91
120
  function pinToBottomNow() {
92
121
  var messagesEl = getMessagesEl();
93
122
  if (!messagesEl) return;
94
- messagesEl.scrollTop = messagesEl.scrollHeight;
123
+ messagesEl.scrollTop = followTarget(messagesEl);
95
124
  }
96
125
 
97
126
  function ensureStickyInfrastructure() {
@@ -110,12 +139,13 @@ function ensureStickyInfrastructure() {
110
139
  }
111
140
  if (!stickyBottomInputBound) {
112
141
  stickyBottomInputBound = true;
113
- var disarmOnUserScroll = function () { disarmStickyBottom(); };
142
+ var disarmOnUserScroll = function () { disarmStickyBottom(); turnAnchor = null; turnAnchorEnded = false; };
114
143
  messagesEl.addEventListener("wheel", disarmOnUserScroll, { passive: true });
115
144
  messagesEl.addEventListener("touchmove", disarmOnUserScroll, { passive: true });
145
+ messagesEl.addEventListener("pointerdown", disarmOnUserScroll, { passive: true });
116
146
  document.addEventListener("keydown", function (event) {
117
- if (!stickyBottom) return;
118
- if (event.key === "PageUp" || event.key === "Home" || event.key === "ArrowUp") disarmStickyBottom();
147
+ if (!stickyBottom && !turnAnchor) return;
148
+ if (event.key === "PageUp" || event.key === "Home" || event.key === "ArrowUp") { disarmStickyBottom(); turnAnchor = null; turnAnchorEnded = false; }
119
149
  });
120
150
  }
121
151
  }
@@ -150,6 +180,7 @@ export function armStickyBottom(durationMs) {
150
180
 
151
181
  export function disarmStickyBottom() {
152
182
  stickyBottom = false;
183
+ if (turnAnchorEnded) { turnAnchor = null; turnAnchorEnded = false; }
153
184
  if (stickyBottomQuietTimer) { clearTimeout(stickyBottomQuietTimer); stickyBottomQuietTimer = null; }
154
185
  if (stickyBottomCeilingTimer) { clearTimeout(stickyBottomCeilingTimer); stickyBottomCeilingTimer = null; }
155
186
  }
@@ -253,6 +284,11 @@ export function scrollToBottom() {
253
284
  requestAnimationFrame(function () { if (stickyBottom && !layoutScrollLock()) pinToBottomNow(); });
254
285
  return;
255
286
  }
287
+ if (activeTurnAnchor(messagesEl) && !isUserScrolledUp) {
288
+ messagesEl.scrollTop = followTarget(messagesEl);
289
+ requestAnimationFrame(function () { if (!layoutScrollLock() && activeTurnAnchor(messagesEl)) messagesEl.scrollTop = followTarget(messagesEl); });
290
+ return;
291
+ }
256
292
  var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
257
293
  if (distFromBottom > 150 || isUserScrolledUp) {
258
294
  if (distFromBottom > 150) isUserScrolledUp = true;
@@ -274,10 +310,14 @@ export function scrollToBottom() {
274
310
  });
275
311
  }
276
312
 
277
- export function forceScrollToBottom() {
313
+ export function forceScrollToBottom(sentTurnEl) {
278
314
  if (prependAnchor || store.get('replayingHistory')) return;
279
- // An explicit new turn supersedes a layout-only scroll receipt.
315
+ // An explicit new turn supersedes a layout-only scroll receipt and any
316
+ // earlier reading position.
280
317
  if (layoutScrollLock()) store.set({ paneLayoutScrollLock: null });
318
+ store.set({ paneLastUserScrollTop: null });
319
+ turnAnchor = sentTurnEl || null;
320
+ turnAnchorEnded = false;
281
321
  armStickyBottom(750);
282
322
  requestAnimationFrame(function () { if (stickyBottom && !layoutScrollLock()) pinToBottomNow(); });
283
323
  }
@@ -20,71 +20,15 @@ export function workerActivityLabel(message, names) {
20
20
  WebSearch: 'Search the web', WebFetch: 'Read web page' };
21
21
  return definition ? definition.noun : (Object.prototype.hasOwnProperty.call(generic, name) ? generic[name] : null) || (typeof name === 'string' ? name.slice(0, 120) : 'Tool activity');
22
22
  }
23
- function workerActivityActionIcon(message, names) {
24
- var match = message.toolId && names[message.toolId];
25
- var name = message.toolName || match && match.name;
26
- var definition = clayToolDefinition(name);
27
- if (definition && typeof definition.icon === 'string' && definition.icon) return definition.icon;
28
- var generic = { Read: 'file-text', read_file: 'file-text', Edit: 'pencil', Write: 'file-plus-2',
29
- Bash: 'square-terminal', exec_command: 'square-terminal', Grep: 'search', Glob: 'search', WebSearch: 'globe', WebFetch: 'globe' };
30
- return Object.prototype.hasOwnProperty.call(generic, name) ? generic[name] : null;
31
- }
32
- export function appendWorkerActivity(body, messages, names) {
33
- var group = document.createElement('section'); group.className = 'worker-activity-group';
34
- var key = String(messages[messages.length - 1].recordIndex);
35
- group.dataset.activityKey = key;
36
- var toggle = document.createElement('button'); toggle.type = 'button'; toggle.className = 'worker-activity-summary';
37
- toggle.dataset.disclosureKey = 'activity-' + key;
38
- var activityIcon = document.createElement('i'); activityIcon.setAttribute('data-lucide', 'activity'); activityIcon.setAttribute('aria-hidden', 'true');
39
- var heading = document.createElement('span'); heading.className = 'worker-activity-heading'; heading.textContent = 'Activity';
40
- var count = document.createElement('span'); count.className = 'worker-activity-count'; count.textContent = messages.length + (messages.length === 1 ? ' recorded step' : ' recorded steps');
41
- var chevron = document.createElement('i'); chevron.className = 'worker-activity-chevron';
42
- chevron.setAttribute('data-lucide', 'chevron-down'); chevron.setAttribute('aria-hidden', 'true');
43
- toggle.append(activityIcon, heading, count, chevron);
44
- toggle.setAttribute('aria-expanded', 'false'); toggle.disabled = messages.length <= 2;
45
- var list = document.createElement('div'); list.className = 'worker-activity-list';
46
- var items = [];
47
- messages.forEach(function (message) {
48
- var item = document.createElement('article'); item.className = 'worker-activity-item worker-tool-message';
49
- item.dataset.recordIndex = String(message.recordIndex);
50
- var phase = message.isError ? 'failed' : message.recordType === 'tool_result' ? 'returned' : 'recorded';
51
- item.dataset.phase = phase;
52
- var row = document.createElement('div'); row.className = 'worker-activity-row';
53
- var actionIcon = workerActivityActionIcon(message, names);
54
- var statusIcon = phase === 'failed' ? 'circle-alert' : phase === 'returned' ? 'corner-down-left' : 'circle-dot';
55
- var glyph = document.createElement('i'); glyph.className = actionIcon ? 'worker-activity-glyph' : 'worker-activity-glyph worker-activity-status'; glyph.setAttribute('aria-hidden', 'true');
56
- glyph.setAttribute('data-lucide', actionIcon || statusIcon);
57
- var label = document.createElement('span'); label.className = 'worker-activity-label'; label.textContent = workerActivityLabel(message, names);
58
- var detail = document.createElement('span'); detail.className = 'worker-activity-detail';
59
- detail.textContent = phase === 'failed' ? 'Failed' : phase === 'returned' ? 'Result returned' : 'Call recorded';
60
- row.append(glyph, label, detail);
61
- if (actionIcon) {
62
- var status = document.createElement('i'); status.className = 'worker-activity-status';
63
- status.setAttribute('data-lucide', statusIcon); status.setAttribute('aria-hidden', 'true'); row.appendChild(status);
64
- }
65
- var evidence = document.createElement('details'); evidence.className = 'worker-activity-evidence'; evidence.dataset.disclosureKey = 'record-' + message.recordIndex;
66
- var summary = document.createElement('summary'); summary.textContent = 'Technical details';
67
- var output = document.createElement('pre'); output.textContent = message.text;
68
- evidence.append(summary, output); item.append(row, evidence); items.push(item); list.appendChild(item);
69
- });
70
- function expand(open) {
71
- toggle.setAttribute('aria-expanded', String(open));
72
- items.forEach(function (item, index) { item.hidden = !open && index < items.length - 2; item.inert = item.hidden; });
73
- }
74
- toggle.addEventListener('click', function () { expand(toggle.getAttribute('aria-expanded') !== 'true'); });
75
- expand(false); group.append(toggle, list); body.appendChild(group);
76
- }
77
23
  export function captureWorkerDisclosures(body) {
78
24
  return {
79
25
  open: Array.from(body.querySelectorAll('details[open]')).map(function (node) { return node.dataset.disclosureKey; }),
80
- groups: Array.from(body.querySelectorAll('.worker-activity-summary[aria-expanded="true"]')).map(function (node) { return node.dataset.disclosureKey; }),
81
26
  focus: document.activeElement && body.contains(document.activeElement) && (document.activeElement.dataset.disclosureKey || document.activeElement.parentElement && document.activeElement.parentElement.dataset.disclosureKey)
82
27
  };
83
28
  }
84
29
  export function restoreWorkerDisclosures(body, saved) {
85
30
  Array.from(body.querySelectorAll('[data-disclosure-key]')).forEach(function (node) {
86
31
  var key = node.dataset.disclosureKey;
87
- if (saved.groups.indexOf(key) >= 0 && node.getAttribute('aria-expanded') === 'false') node.click();
88
32
  if (saved.open.indexOf(key) >= 0) node.open = true;
89
33
  if (saved.focus === key) {
90
34
  var target = node.tagName === 'DETAILS' ? node.querySelector('summary') : node;
@@ -1,4 +1,4 @@
1
- import { appendWorkerActivity, workerActivityNames } from './split-view-worker-activity.js';
1
+ import { workerActivityLabel, workerActivityNames } from './split-view-worker-activity.js';
2
2
  import { renderMarkdown } from './markdown.js';
3
3
 
4
4
  // Reassemble bounded record fragments and contiguous streaming deltas before
@@ -17,13 +17,26 @@ export function workerTranscriptRows(messages) {
17
17
  return rows;
18
18
  }
19
19
 
20
- export function appendWorkerMessage(body, message) {
21
- if (message.role === 'tool') { appendWorkerActivity(body, [message], workerActivityNames([message])); return; }
20
+ export function appendWorkerMessage(body, message, names) {
21
+ if (message.role === 'tool') {
22
+ var article = document.createElement('article');
23
+ article.className = 'message worker-readonly-message worker-tool-message';
24
+ article.dataset.recordIndex = String(message.recordIndex);
25
+ article.dataset.phase = message.isError ? 'failed' : message.recordType === 'tool_result' ? 'returned' : 'recorded';
26
+ var details = document.createElement('details');
27
+ details.dataset.disclosureKey = 'record-' + message.recordIndex;
28
+ var summary = document.createElement('summary');
29
+ summary.textContent = workerActivityLabel(message, names || workerActivityNames([message])) +
30
+ (message.isError ? ' · Failed' : message.recordType === 'tool_result' ? ' · Result returned' : ' · Call recorded');
31
+ var output = document.createElement('pre'); output.textContent = message.text;
32
+ details.append(summary, output); article.appendChild(details); body.appendChild(article);
33
+ return;
34
+ }
22
35
  var article = document.createElement('article');
23
36
  article.className = 'message worker-readonly-message ' + (message.role === 'user' ? 'msg-user' : 'msg-assistant');
24
37
  if (Number.isInteger(message.recordIndex)) article.dataset.recordIndex = String(message.recordIndex);
25
38
  var author = document.createElement('strong'); author.className = 'author';
26
- author.textContent = message.role === 'user' ? 'You' : 'Worker';
39
+ author.textContent = message.role === 'user' ? 'Driver' : 'Worker';
27
40
  var content = document.createElement('div'); content.className = 'message-body md-content';
28
41
  // Recorded HTML must not create controls, scoped overlays or active embeds.
29
42
  // Keep the shared Markdown parser and its sanitization, with a narrower
@@ -36,11 +49,6 @@ export function appendWorkerMessage(body, message) {
36
49
  }
37
50
 
38
51
  export function appendWorkerTranscript(body, messages) {
39
- var rows = workerTranscriptRows(messages), names = workerActivityNames(rows), pending = [];
40
- function flush() { if (pending.length) appendWorkerActivity(body, pending, names); pending = []; }
41
- rows.forEach(function (message) {
42
- if (message.role === 'tool') pending.push(message);
43
- else { flush(); appendWorkerMessage(body, message); }
44
- });
45
- flush();
52
+ var rows = workerTranscriptRows(messages), names = workerActivityNames(rows);
53
+ rows.forEach(function (message) { appendWorkerMessage(body, message, names); });
46
54
  }
@@ -71,6 +71,54 @@ export function workerRemovalTarget(container, driverOrigin, workerOrigin, gener
71
71
  bindingGeneration: source.binding.generation, bindingRequestId: source.binding.requestId,
72
72
  viewRevision: store.get('projectSplitViewRevision'), socketGeneration: store.get('projectSplitViewSocketGeneration') };
73
73
  }
74
+ // One introduction per Driver in this page, independent of transcript refreshes.
75
+ export function showCreatedWorkerViewer(group) {
76
+ var state = store.snap(), roles = splitGroupRoles(group);
77
+ if (!roles || state.paneMode || state.dmMode || state.replayingHistory || !state.connected ||
78
+ state.activeSessionId !== roles.driverId || document.hidden) return;
79
+ var workerId = roles.workerIds[roles.workerIds.length - 1];
80
+ var worker = sessions().find(function (item) { return item.id === workerId; });
81
+ var anchors = group.pairAnchors, anchor = anchors && (anchors.workers || [anchors.worker])[roles.workerIds.length - 1];
82
+ var origin = worker && worker.sessionOriginId || anchor && anchor.origin;
83
+ if (!origin) return;
84
+ var driverIdentity = sessions().find(function (item) { return item.id === roles.driverId; });
85
+ var driverOrigin = driverIdentity && driverIdentity.sessionOriginId || anchors && anchors.driver && anchors.driver.origin;
86
+ if (!driverOrigin) return;
87
+ var key = JSON.stringify([state.myUserId, state.currentSlug, driverOrigin]);
88
+ var seen = state.introducedWorkerDrivers || [];
89
+ if (seen.indexOf(key) >= 0) return;
90
+ store.set({ introducedWorkerDrivers: seen.concat(key).slice(-64) });
91
+ if (state.createdWorkerViewer || state.splitViewWorkerConversation) return;
92
+ store.set({ createdWorkerViewer: { driverId: roles.driverId, driverOrigin: driverOrigin, workerId: workerId, workerOrigin: origin,
93
+ generation: worker ? worker.workerGeneration : null,
94
+ ownerId: state.myUserId, slug: state.currentSlug, socketGeneration: state.projectSplitViewSocketGeneration } });
95
+ var driver = sessions().find(function (item) { return item.id === roles.driverId; });
96
+ var pane = driver && document.querySelector('.split-pane[data-session-origin="' + CSS.escape(driver.sessionOriginId) + '"]');
97
+ if (pane) renderPaneWorkers(pane, driver);
98
+ }
99
+ function introduceWorkerViewer(container, driver) {
100
+ var pending = store.get('createdWorkerViewer'), state = store.snap();
101
+ if (!pending || pending.driverId !== driver.id) return;
102
+ var worker = sessions().find(function (item) { return item.id === pending.workerId; });
103
+ if (!worker) return;
104
+ store.set({ createdWorkerViewer: null });
105
+ var group = (state.splitGroups || []).find(function (item) {
106
+ var roles = splitGroupRoles(item);
107
+ return roles && roles.driverId === driver.id && roles.workerIds.indexOf(worker.id) >= 0;
108
+ });
109
+ if (!group || driver.sessionOriginId !== pending.driverOrigin || worker.sessionOriginId !== pending.workerOrigin || pending.generation !== null && worker.workerGeneration !== pending.generation ||
110
+ state.splitViewWorkerConversation || document.hidden || !container.isConnected || container.hidden || container.inert ||
111
+ driver.hidden || worker.hidden || exactWorker(driver, worker.sessionOriginId, worker.workerGeneration) !== worker) return;
112
+ var chip = Array.from(container.querySelectorAll('.split-view-worker-chip')).find(function (item) { return item.dataset.workerOrigin === worker.sessionOriginId; });
113
+ if (chip) open(chip, driver, worker, false);
114
+ }
115
+ function syncWorkerSelection() {
116
+ var state = store.get('splitViewWorkerConversation');
117
+ document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
118
+ chip.setAttribute('aria-expanded', String(!!state && chip.dataset.workerOrigin === state.workerOriginId &&
119
+ chip.dataset.workerGeneration === String(state.generation)));
120
+ });
121
+ }
74
122
  function popover() { return document.getElementById("split-view-worker-popover"); }
75
123
  function close(returnFocus) {
76
124
  var state = store.get('splitViewWorkerConversation');
@@ -84,6 +132,7 @@ function close(returnFocus) {
84
132
  if (state.fillTimer) clearTimeout(state.fillTimer);
85
133
  if (state.requestTimer) clearTimeout(state.requestTimer);
86
134
  store.set({ splitViewWorkerConversation: null });
135
+ syncWorkerSelection();
87
136
  var element = popover(); if (element) element.remove();
88
137
  if (returnFocus) {
89
138
  var chip = document.querySelector('.split-pane[data-session-origin="' + CSS.escape(state.driverOriginId) + '"] [data-worker-origin="' + CSS.escape(state.workerOriginId) + '"]');
@@ -135,7 +184,7 @@ function requestPage(state, before, append, refresh) {
135
184
  }
136
185
  }
137
186
  }
138
- function open(chip, driver, worker) {
187
+ function open(chip, driver, worker, takeFocus) {
139
188
  var existing = store.get('splitViewWorkerConversation');
140
189
  if (existing && existing.driverOriginId === driver.sessionOriginId && existing.workerOriginId === worker.sessionOriginId && existing.generation === worker.workerGeneration) { close(true); return; }
141
190
  if (existing) close(false);
@@ -146,7 +195,8 @@ function open(chip, driver, worker) {
146
195
  store.set({ splitViewWorkerConversation: state }); ensurePopover(); renderContent();
147
196
  var element = popover(); position(element, chip);
148
197
  requestPage(state, undefined, false);
149
- var closeButton = element && element.querySelector('[aria-label="Close Worker conversation"]'); if (closeButton) closeButton.focus();
198
+ syncWorkerSelection();
199
+ var closeButton = element && element.querySelector('[aria-label="Close Worker conversation"]'); if (closeButton && takeFocus !== false) closeButton.focus();
150
200
  }
151
201
  function ensurePopover() {
152
202
  var state = store.get('splitViewWorkerConversation');
@@ -368,6 +418,7 @@ export function renderPaneWorkers(container, driver) {
368
418
  });
369
419
  root.appendChild(controls);
370
420
  }
421
+ introduceWorkerViewer(container, driver);
371
422
  var conversation = store.get('splitViewWorkerConversation');
372
423
  if (conversation) ensurePopover();
373
424
  }
@@ -386,6 +437,11 @@ window.addEventListener("scroll", function (event) {
386
437
  reanchorPopover();
387
438
  }, true);
388
439
  store.subscribe(function (state, previous) {
440
+ var introduction = state.createdWorkerViewer;
441
+ if (introduction && (!state.connected || introduction.slug !== state.currentSlug || introduction.ownerId !== state.myUserId ||
442
+ introduction.socketGeneration !== state.projectSplitViewSocketGeneration || introduction.driverId !== state.activeSessionId)) {
443
+ store.set({ createdWorkerViewer: null });
444
+ }
389
445
  if (state.paneBindingSessions !== previous.paneBindingSessions || state.connected !== previous.connected) {
390
446
  document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
391
447
  var worker = sessions().find(function (item) { return item.sessionOriginId === chip.dataset.workerOrigin; });
@@ -1,4 +1,5 @@
1
1
  // New execution pairs keep the Driver's current presentation; Workers live in pills.
2
+ import { showCreatedWorkerViewer } from './split-view-workers.js';
2
3
  import { store } from './store.js';
3
4
  import { getWs } from './ws-ref.js';
4
5
  import { getCachedSessions } from './sidebar-sessions.js';
@@ -24,6 +25,7 @@ function reconcileCreatedWorkerPresentation() {
24
25
  (session.ownerId || null) === (state.myUserId || null); });
25
26
  if (state.sessionListProjectSlug !== state.currentSlug || !driver || !driver.sessionOriginId || !getWs() || getWs().readyState !== 1) return false;
26
27
  store.set({ createdWorkerPresentation: null });
28
+ showCreatedWorkerViewer(pending.group);
27
29
  if (state.activeSessionId === driver.id && !state.splitPanes) return true;
28
30
  var view = (state.projectSplitViews || []).find(function (item) {
29
31
  return item.sessions.some(function (ref) { return ref.id === driver.id && ref.sessionOriginId === driver.sessionOriginId; });