clay-server 5.3.0-beta.6 → 5.3.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 (111) hide show
  1. package/lib/project-admin-overview.js +22 -11
  2. package/lib/project-background-tasks.js +76 -0
  3. package/lib/project-connection.js +1 -1
  4. package/lib/project-sessions.js +12 -5
  5. package/lib/project-split-views.js +12 -3
  6. package/lib/project-view-store.js +7 -5
  7. package/lib/project-worker-conversation.js +5 -0
  8. package/lib/public/app.js +7 -0
  9. package/lib/public/css/contextual-clay.css +2 -1
  10. package/lib/public/css/mate-conversation.css +1 -1
  11. package/lib/public/css/pending-message-queue.css +24 -19
  12. package/lib/public/css/project-admin-overview.css +42 -13
  13. package/lib/public/css/shared-browser.css +31 -8
  14. package/lib/public/css/split-view-layout.css +92 -7
  15. package/lib/public/css/tool-pane.css +4 -7
  16. package/lib/public/index.html +5 -5
  17. package/lib/public/modules/agent-message-bubbles.js +96 -0
  18. package/lib/public/modules/app-header.js +6 -0
  19. package/lib/public/modules/app-messages.js +26 -9
  20. package/lib/public/modules/background-task-previews.js +8 -0
  21. package/lib/public/modules/background-task-viewer.js +97 -0
  22. package/lib/public/modules/background-tasks-ui.js +34 -125
  23. package/lib/public/modules/chat-bubble-renderer.js +11 -5
  24. package/lib/public/modules/contextual-clay-geometry.js +7 -3
  25. package/lib/public/modules/pane-activation.js +5 -3
  26. package/lib/public/modules/pane-header-tooltips.js +32 -0
  27. package/lib/public/modules/pending-message-queue-drag.js +69 -0
  28. package/lib/public/modules/pending-message-queue.js +3 -33
  29. package/lib/public/modules/permission-control.js +6 -5
  30. package/lib/public/modules/project-admin-overview.js +125 -10
  31. package/lib/public/modules/project-grouping.js +9 -0
  32. package/lib/public/modules/server-settings-controls.js +33 -4
  33. package/lib/public/modules/session-folder-toolbar.js +1 -1
  34. package/lib/public/modules/session-tool-item.js +86 -0
  35. package/lib/public/modules/settings-select.js +2 -1
  36. package/lib/public/modules/shared-browser-tabs.js +37 -21
  37. package/lib/public/modules/sidebar-projects.js +1 -10
  38. package/lib/public/modules/split-session-empty-pane.js +47 -0
  39. package/lib/public/modules/split-view-create-menu.js +74 -65
  40. package/lib/public/modules/split-view-layout-scope.js +4 -0
  41. package/lib/public/modules/split-view-picker-actions.js +71 -0
  42. package/lib/public/modules/split-view-picker-render.js +81 -0
  43. package/lib/public/modules/split-view-presentation-renderer.js +8 -8
  44. package/lib/public/modules/split-view-worker-activity.js +2 -2
  45. package/lib/public/modules/split-view-worker-transcript.js +70 -33
  46. package/lib/public/modules/split-view-workers.js +6 -4
  47. package/lib/public/modules/terminal-viewport.js +14 -0
  48. package/lib/public/modules/terminal.js +3 -4
  49. package/lib/public/modules/theme-palette.js +177 -0
  50. package/lib/public/modules/theme.js +1 -174
  51. package/lib/public/modules/tool-pane-chrome.js +7 -1
  52. package/lib/public/modules/tools.js +5 -89
  53. package/lib/public/modules/tooltip.js +32 -6
  54. package/lib/public/modules/wireframe-workbench.js +2 -2
  55. package/lib/public/modules/worker-message-previews.js +39 -0
  56. package/lib/public/pane-assets/app-5W6KJXK5.js +1 -0
  57. package/lib/public/pane-assets/chunk-2IJUTAB6.js +2 -0
  58. package/lib/public/pane-assets/{chunk-BMHTBRMO.js → chunk-7OVNC5HT.js} +1 -1
  59. package/lib/public/pane-assets/chunk-BQN7B62H.js +3 -0
  60. package/lib/public/pane-assets/{chunk-2AIWCI6Y.js → chunk-EMLN2Y7L.js} +1 -1
  61. package/lib/public/pane-assets/{chunk-VKYBAVWT.js → chunk-ENPAHXLB.js} +1 -1
  62. package/lib/public/pane-assets/chunk-EPBGGZRT.js +1 -0
  63. package/lib/public/pane-assets/chunk-EYTE56IA.js +1 -0
  64. package/lib/public/pane-assets/{chunk-6NYCNWVF.js → chunk-FQTNR4ZS.js} +1 -1
  65. package/lib/public/pane-assets/chunk-L6LSA5WI.js +1 -0
  66. package/lib/public/pane-assets/chunk-M57UIGZV.js +1 -0
  67. package/lib/public/pane-assets/chunk-MGTFTEJP.js +1 -0
  68. package/lib/public/pane-assets/chunk-MMG6MYCN.js +1 -0
  69. package/lib/public/pane-assets/chunk-MZLMEXMW.js +1 -0
  70. package/lib/public/pane-assets/chunk-OG3E5SVA.js +166 -0
  71. package/lib/public/pane-assets/chunk-RGAFZ3T2.js +1 -0
  72. package/lib/public/pane-assets/chunk-RNDRURMF.js +1 -0
  73. package/lib/public/pane-assets/{chunk-PV4PCH2V.js → chunk-SJOT74VW.js} +1 -1
  74. package/lib/public/pane-assets/{chunk-LDIZLKB2.js → chunk-SJTGHIN5.js} +1 -1
  75. package/lib/public/pane-assets/chunk-SLMAFKI2.js +1 -0
  76. package/lib/public/pane-assets/chunk-U3I5KY3B.js +1 -0
  77. package/lib/public/pane-assets/{chunk-VNAV23CM.js → chunk-UPXU2IPC.js} +1 -1
  78. package/lib/public/pane-assets/{chunk-TMGSDFJI.js → chunk-VEBYD7TL.js} +1 -1
  79. package/lib/public/pane-assets/chunk-XBHYNXXT.js +1 -0
  80. package/lib/public/pane-assets/manifest.json +411 -399
  81. package/lib/public/pane-assets/style-BSKKK54Q.css +1 -0
  82. package/lib/sdk-message-processor.js +5 -2
  83. package/lib/server-storage.js +27 -3
  84. package/lib/server.js +4 -2
  85. package/lib/sessions.js +1 -1
  86. package/lib/storage-usage.js +29 -7
  87. package/lib/terminal-manager.js +2 -2
  88. package/lib/worker-message-previews.js +50 -0
  89. package/lib/ws-schema.js +1 -1
  90. package/lib/yoke/adapters/claude-worker.js +8 -0
  91. package/lib/yoke/adapters/claude.js +17 -2
  92. package/lib/yoke/claude-task-reader.js +94 -0
  93. package/lib/yoke/interface.js +1 -0
  94. package/package.json +1 -1
  95. package/lib/public/pane-assets/app-7UZ727NA.js +0 -1
  96. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +0 -1
  97. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +0 -1
  98. package/lib/public/pane-assets/chunk-6XK2OATZ.js +0 -1
  99. package/lib/public/pane-assets/chunk-BKIDR2ME.js +0 -1
  100. package/lib/public/pane-assets/chunk-GEFCFZEN.js +0 -1
  101. package/lib/public/pane-assets/chunk-GN2NQEVC.js +0 -2
  102. package/lib/public/pane-assets/chunk-IMLMBPT4.js +0 -166
  103. package/lib/public/pane-assets/chunk-K5S6AILM.js +0 -1
  104. package/lib/public/pane-assets/chunk-OWNNLZ36.js +0 -1
  105. package/lib/public/pane-assets/chunk-P2C3LOWL.js +0 -3
  106. package/lib/public/pane-assets/chunk-R4LMPTKU.js +0 -1
  107. package/lib/public/pane-assets/chunk-VWTHIQCE.js +0 -1
  108. package/lib/public/pane-assets/chunk-WM3K5AFH.js +0 -1
  109. package/lib/public/pane-assets/chunk-XOB3O327.js +0 -1
  110. package/lib/public/pane-assets/chunk-XOOUCFYT.js +0 -1
  111. package/lib/public/pane-assets/style-ZM7OOU3M.css +0 -1
@@ -1,83 +1,92 @@
1
- // Pane-header picker for explicitly choosing real Driver sessions for a Split view.
1
+ // Direction menu, then a centered session picker inside the reserved empty pane.
2
2
  import { store } from './store.js';
3
- import { getCachedSessions } from './sidebar-sessions.js';
4
- import { viewForOrigin } from './split-view-presentation-lookup.js';
5
- import { iconHtml } from './icons.js';
3
+ import { retryOptions } from './session-create.js';
4
+ import { pickerReceipt, pickerCurrent, insertPickerSession, createPickerSession } from './split-view-picker-actions.js';
5
+ import { renderPicker, renderPickerResults } from './split-view-picker-render.js';
6
+ import { showToast } from './utils.js';
6
7
 
7
8
  function menuElement() { return document.getElementById('split-view-create-menu'); }
8
9
  function close(returnFocus) {
9
10
  var menu = menuElement(); if (!menu) return;
10
- menu.hidden = true; store.set({ projectSplitViewCreateSelection: [] });
11
- if (returnFocus && menu.clayCreateAnchor && menu.clayCreateAnchor.isConnected) menu.clayCreateAnchor.focus();
12
- menu.clayCreateAnchor = null;
11
+ var state = store.get('splitSessionPicker');
12
+ var update = { splitSessionPicker: null, projectSplitViewCreateSelection: [] };
13
+ if (returnFocus && state && pickerCurrent(state.receipt) && !store.get('projectSplitViewPending')) update.projectSplitViewFocusedOrigin = state.priorFocus || null;
14
+ var anchor = menu.clayCreateAnchor; menu.clayCreateAnchor = null;
15
+ menu.hidden = true; store.set(update);
16
+ if (menu.contains(document.activeElement)) document.activeElement.blur();
17
+ if (menu.parentNode !== document.body) document.body.appendChild(menu);
18
+ if (anchor) anchor.setAttribute('aria-expanded', 'false');
19
+ document.dispatchEvent(new CustomEvent('clay-split-picker-layout'));
20
+ if (returnFocus && anchor && anchor.isConnected) anchor.focus();
13
21
  }
14
- function refreshCreateButton(menu) {
15
- var selected = store.get('projectSplitViewCreateSelection') || [], action = menu.querySelector('[data-create-split-view]');
16
- action.disabled = selected.length < 2 || selected.length > 8;
17
- action.textContent = 'Create Split view (' + selected.length + ')';
22
+ function position() {
23
+ var menu = menuElement(), anchor = menu && menu.clayCreateAnchor;
24
+ if (!menu || menu.hidden) return;
25
+ if (!anchor || !anchor.isConnected) { close(false); return; }
26
+ var state = store.get('splitSessionPicker');
27
+ if (state && state.side) { menu.style.left = ''; menu.style.top = ''; return; }
28
+ var rect = anchor.getBoundingClientRect(), width = menu.offsetWidth;
29
+ menu.style.left = Math.max(8, Math.min(rect.right - width, innerWidth - width - 8)) + 'px';
30
+ menu.style.top = Math.max(8, Math.min(rect.bottom + 5, innerHeight - menu.offsetHeight - 8)) + 'px';
18
31
  }
19
32
  function open(anchor) {
33
+ var receipt = pickerReceipt(anchor);
34
+ if (!receipt || !pickerCurrent(receipt)) { showToast('Wait for this View to finish loading, then try again.', 'warn'); return; }
20
35
  var menu = menuElement();
36
+ if (menu && !menu.hidden && menu.clayCreateAnchor === anchor) { close(true); return; }
37
+ if (menu && !menu.hidden) close(false);
21
38
  if (!menu) {
22
- menu = document.createElement('section'); menu.id = 'split-view-create-menu'; menu.className = 'split-view-arrange';
23
- menu.hidden = true; menu.setAttribute('role', 'dialog'); menu.setAttribute('aria-label', 'Arrange sessions');
24
- var header = document.createElement('div'); header.className = 'split-view-arrange-header';
25
- var title = document.createElement('strong'); title.textContent = 'Arrange sessions'; header.appendChild(title);
26
- var closeButton = document.createElement('button'); closeButton.type = 'button'; closeButton.setAttribute('aria-label', 'Close arrangement picker'); closeButton.innerHTML = iconHtml('x'); header.appendChild(closeButton); menu.appendChild(header);
27
- var list = document.createElement('div'); list.className = 'split-view-arrange-grid'; list.setAttribute('role', 'group'); list.setAttribute('aria-label', 'Choose sessions');
28
- list.style.cssText = 'display:flex;flex-direction:column;max-height:230px;overflow:auto'; menu.appendChild(list);
29
- var create = document.createElement('button'); create.type = 'button'; create.dataset.createSplitView = 'true'; menu.appendChild(create);
30
- document.body.appendChild(menu);
39
+ menu = document.createElement('section'); menu.id = 'split-view-create-menu'; menu.className = 'split-view-arrange split-session-picker'; menu.hidden = true;
40
+ menu.setAttribute('role', 'dialog'); menu.setAttribute('aria-label', 'Add a session split'); document.body.appendChild(menu);
31
41
  }
32
- menu.clayCreateAnchor = anchor;
33
- var list = menu.querySelector('[aria-label="Choose sessions"]'); list.textContent = '';
34
- var activeView = (store.get('projectSplitViews') || []).find(function (view) { return view.id === store.get('projectSplitViewActiveId'); });
35
- var currentId = Number(store.get('activeSessionId'));
36
- var current = (getCachedSessions() || []).find(function (session) { return session.id === currentId; });
37
- var selected = activeView ? activeView.sessions.map(function (item) { return item.sessionOriginId; }) :
38
- current && current.sessionRole !== 'worker' && !current.isMate && !current.isTui ? [current.sessionOriginId] : [];
39
- var candidates = (getCachedSessions() || []).filter(function (session) {
40
- if (!session.sessionOriginId || session.sessionRole === 'worker' || session.isMate || session.isTui) return false;
41
- var owner = viewForOrigin(session.sessionOriginId); return !owner || activeView && owner.id === activeView.id;
42
- });
43
- candidates.forEach(function (session) {
44
- var choice = document.createElement('button'); choice.type = 'button'; choice.dataset.createSessionOrigin = session.sessionOriginId;
45
- choice.setAttribute('aria-pressed', selected.indexOf(session.sessionOriginId) >= 0 ? 'true' : 'false');
46
- choice.textContent = session.title || 'Session'; list.appendChild(choice);
47
- });
48
- store.set({ projectSplitViewCreateSelection: selected }); refreshCreateButton(menu); menu.hidden = false;
49
- var rect = anchor.getBoundingClientRect(), width = Math.min(280, window.innerWidth - 16);
50
- menu.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)) + 'px';
51
- menu.style.top = Math.max(8, Math.min(rect.bottom + 4, window.innerHeight - menu.offsetHeight - 8)) + 'px';
52
- var first = menu.querySelector('[data-create-session-origin]'); if (first) first.focus();
42
+ receipt.reservationId = 'split-picker-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
43
+ menu.clayCreateAnchor = anchor; anchor.setAttribute('aria-expanded', 'true');
44
+ store.set({ splitSessionPicker: { receipt: receipt, side: null, query: '', error: '', priorFocus: store.get('projectSplitViewFocusedOrigin') } });
45
+ renderPicker(menu); menu.hidden = false; position(); menu.querySelector('[data-split-side]').focus();
53
46
  }
54
47
  function onClick(event) {
55
- var target = event.target;
56
- var menu = menuElement(); if (!menu || menu.hidden) return;
57
- if (menu.contains(target)) {
58
- var closeButton = target.closest('[aria-label="Close arrangement picker"]');
59
- if (closeButton) { event.preventDefault(); close(true); return; }
60
- var choice = target.closest('[data-create-session-origin]');
61
- if (choice) {
62
- event.preventDefault(); var selected = (store.get('projectSplitViewCreateSelection') || []).slice(), origin = choice.dataset.createSessionOrigin, index = selected.indexOf(origin);
63
- if (index === -1 && selected.length >= 8) return;
64
- if (index === -1) selected.push(origin); else selected.splice(index, 1);
65
- store.set({ projectSplitViewCreateSelection: selected }); choice.setAttribute('aria-pressed', selected.indexOf(origin) >= 0 ? 'true' : 'false'); refreshCreateButton(menu); return;
66
- }
67
- if (target.closest('[data-create-split-view]') && !target.closest('[data-create-split-view]').disabled) {
68
- event.preventDefault(); document.dispatchEvent(new CustomEvent('clay-split-view-create', { detail: { origins: (store.get('projectSplitViewCreateSelection') || []).slice() } })); close(true);
69
- }
70
- return;
48
+ var menu = menuElement(), state = store.get('splitSessionPicker'); if (!menu || menu.hidden || !state) return;
49
+ var button = event.target.closest && event.target.closest('button');
50
+ if (!menu.contains(event.target)) { if (!state.side && (!event.target.closest || !event.target.closest('.split-view-arrange-trigger'))) close(false); return; }
51
+ if (!button || button.disabled) return;
52
+ event.preventDefault();
53
+ if (button.dataset.splitClose) { close(true); return; }
54
+ if (!pickerCurrent(state.receipt)) { close(true); return; }
55
+ if (button.dataset.splitSide) {
56
+ store.set({ splitSessionPicker: Object.assign({}, state, { side: button.dataset.splitSide, query: '' }), projectSplitViewFocusedOrigin: null }); renderPicker(menu); document.dispatchEvent(new CustomEvent('clay-split-picker-layout')); position(); menu.querySelector('input').focus(); retryOptions(); return;
71
57
  }
72
- if (target.closest && target.closest('.split-view-arrange-trigger')) return;
73
- close(false);
58
+ if (button.dataset.splitRetry) { retryOptions(); return; }
59
+ var accepted = button.dataset.splitSession ? insertPickerSession(state.receipt, state.side, Number(button.dataset.splitSession)) : button.dataset.splitVendor ? createPickerSession(state.receipt, state.side, button.dataset.splitVendor) : false;
60
+ if (accepted) renderPickerResults(menu);
61
+ else { store.set({ splitSessionPicker: Object.assign({}, state, { error: 'The split could not be added. Check the View and try again.' }) }); renderPickerResults(menu); }
62
+ }
63
+ function onKey(event) {
64
+ var menu = menuElement(), state = store.get('splitSessionPicker'); if (!menu || menu.hidden || !state) return;
65
+ if (event.key === 'Escape') { event.preventDefault(); event.stopImmediatePropagation(); close(true); return; }
66
+ if (!menu.contains(event.target)) return;
67
+ var keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
68
+ if (keys.indexOf(event.key) < 0 && !(event.key === 'Enter' && event.target.tagName === 'INPUT')) return;
69
+ var items = Array.from(menu.querySelectorAll('[data-split-side], [data-split-session], [data-split-vendor]')).filter(function (button) { return !button.disabled; });
70
+ if (!items.length) return;
71
+ if (event.target.tagName === 'INPUT' && (event.key === 'Home' || event.key === 'End')) return;
72
+ event.preventDefault();
73
+ var index = items.indexOf(document.activeElement);
74
+ if (event.key === 'Enter') { items[0].click(); return; }
75
+ var next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : index < 0 ? event.key === 'ArrowUp' ? items.length - 1 : 0 : (index + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
76
+ items[next].focus();
74
77
  }
75
78
  export function initSplitViewCreateMenu() {
76
79
  document.addEventListener('clay-split-view-open-create', function (event) { if (event.detail && event.detail.anchor) open(event.detail.anchor); });
77
- document.addEventListener('click', onClick, true);
78
- document.addEventListener('keydown', function (event) {
79
- if (event.key !== 'Escape' || !menuElement() || menuElement().hidden) return;
80
- event.preventDefault(); event.stopImmediatePropagation(); close(true);
81
- }, true);
82
- window.addEventListener('resize', function () { if (menuElement() && !menuElement().hidden) close(false); });
80
+ document.addEventListener('click', onClick, true); document.addEventListener('keydown', onKey, true);
81
+ document.addEventListener('input', function (event) {
82
+ var state = store.get('splitSessionPicker'); if (!state || !event.target.matches('#split-view-create-menu input')) return;
83
+ store.set({ splitSessionPicker: Object.assign({}, state, { query: event.target.value }) });
84
+ var label = menuElement().querySelector('.split-session-section'); label.textContent = event.target.value ? 'Search results' : 'Recent sessions'; renderPickerResults(menuElement()); position();
85
+ });
86
+ window.addEventListener('resize', position);
87
+ store.subscribe(function (state, previous) {
88
+ if (!state.splitSessionPicker) return;
89
+ if (!pickerCurrent(state.splitSessionPicker.receipt)) { close(false); return; }
90
+ if (state.sessionCreateOptions !== previous.sessionCreateOptions || state.projectSplitViewPending !== previous.projectSplitViewPending || state.paneBindingSessions !== previous.paneBindingSessions || state.splitSessionCreation !== previous.splitSessionCreation) { renderPickerResults(menuElement()); position(); }
91
+ });
83
92
  }
@@ -36,3 +36,7 @@ export function presentationTree(view) {
36
36
  JSON.stringify(receipt) === JSON.stringify(layoutScope(view)) ?
37
37
  store.get('projectSplitViewPreviewTree') : view && view.tree;
38
38
  }
39
+ export function bindingMatches(left, right) {
40
+ return !!left && !!right && left.sessionId === right.sessionId && left.sessionOriginId === right.sessionOriginId &&
41
+ left.generation === right.generation && left.requestId === right.requestId;
42
+ }
@@ -0,0 +1,71 @@
1
+ // Exact-context session insertion through the existing View and creation services.
2
+ import { store } from './store.js';
3
+ import { getWs } from './ws-ref.js';
4
+ import { getCachedSessions } from './sidebar-sessions.js';
5
+ import { activeSavedView } from './view-workspace-state.js';
6
+ import { layoutScope, currentLayoutScope } from './split-view-layout-scope.js';
7
+ import { viewForOrigin } from './split-view-presentation-lookup.js';
8
+ import { applyPresentationDrop } from './split-view-presentation-drop.js';
9
+ import { mutation } from './view-mutations.js';
10
+ import { checkComposerDeparture } from './session-rich-drafts.js';
11
+ import { createOptions } from './session-create.js';
12
+ import { showToast } from './utils.js';
13
+
14
+ export function pickerReceipt(anchor) {
15
+ var view = activeSavedView(), pane = anchor && anchor.closest('.split-pane'), frame = pane && pane.querySelector('.split-pane-frame');
16
+ var origin = pane && pane.dataset.presentationOrigin || frame && frame.dataset.sessionOriginId || store.get('sessionOriginId');
17
+ return view && view.sessions.some(function (session) { return session.sessionOriginId === origin; }) ? { scope: layoutScope(view), origin: origin } : null;
18
+ }
19
+ export function pickerCurrent(receipt) {
20
+ var picker = store.get('splitSessionPicker');
21
+ return receipt && (!receipt.reservationId || picker && picker.receipt.reservationId === receipt.reservationId) && !store.get('paneMode') && currentLayoutScope(receipt.scope) && activeSavedView().sessions.some(function (session) { return session.sessionOriginId === receipt.origin; });
22
+ }
23
+ export function pickerCapacity() {
24
+ var view = activeSavedView();
25
+ return !!view && view.sessions.length + (view.workspace.linkedPair ? view.workspace.linkedPair.workerOrigins.length : 0) < 8;
26
+ }
27
+ function checkDrafts() {
28
+ checkComposerDeparture();
29
+ document.querySelectorAll('#split-host .split-pane:not(.split-pane-cached) .split-pane-frame').forEach(function (frame) { if (frame.clayPane) frame.clayPane.checkDraft(); });
30
+ }
31
+ export function insertPickerSession(receipt, side, id) {
32
+ if (['left', 'right', 'top', 'bottom'].indexOf(side) < 0 || !pickerCurrent(receipt) || !pickerCapacity() || store.get('projectSplitViewPending') || store.get('projectSplitViewError')) return false;
33
+ var view = activeSavedView(), session = (getCachedSessions() || []).find(function (item) { return item.id === id; });
34
+ if (!session || session.sessionRole === 'worker' || session.isMate || session.isTui || (session.ownerId || null) !== (store.get('myUserId') || null) || view.sessions.some(function (ref) { return ref.sessionOriginId === session.sessionOriginId; })) return false;
35
+ var source = viewForOrigin(session.sessionOriginId);
36
+ return applyPresentationDrop({ origin: session.sessionOriginId, sessionId: id, sourceViewId: source && source.id },
37
+ { origin: receipt.origin, viewId: view.id, side: side }, mutation, checkDrafts) === true;
38
+ }
39
+ export function createPickerSession(receipt, side, vendor) {
40
+ if (['left', 'right', 'top', 'bottom'].indexOf(side) < 0 || !pickerCurrent(receipt) || !pickerCapacity() || store.get('splitSessionCreation') || store.get('projectSplitViewPending')) return false;
41
+ if (!createOptions().vendors.some(function (item) { return item.id === vendor && item.installed; })) return false;
42
+ try { checkDrafts(); } catch (error) { showToast(error.message, 'warn'); return false; }
43
+ var id = 'split-create-' + (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : Date.now().toString(36) + '-' + Math.random().toString(36).slice(2));
44
+ store.set({ splitSessionCreation: { receipt: receipt, side: side, requestId: id, createdId: null } });
45
+ try { getWs().send(JSON.stringify({ type: 'new_session', slug: store.get('currentSlug'), requestId: id, vendor: vendor, forceNew: true, activate: false })); }
46
+ catch (error) { store.set({ splitSessionCreation: null }); return false; }
47
+ setTimeout(function () {
48
+ var pending = store.get('splitSessionCreation');
49
+ if (pending && pending.requestId === id) {
50
+ store.set({ splitSessionCreation: null });
51
+ showToast('Creation was not confirmed. Check recent sessions before trying again.', 'warn');
52
+ }
53
+ }, 30000);
54
+ return true;
55
+ }
56
+ export function handleSplitSessionCreated(message) {
57
+ var pending = store.get('splitSessionCreation');
58
+ if (message.type !== 'new_session_result' || !pending || message.requestId !== pending.requestId) return false;
59
+ if (!message.ok) { store.set({ splitSessionCreation: null }); showToast(message.error || 'The session could not be created.', 'error'); return true; }
60
+ store.set({ splitSessionCreation: Object.assign({}, pending, { createdId: message.sessionId }) });
61
+ finishPickerCreation(); return true;
62
+ }
63
+ export function finishPickerCreation() {
64
+ var pending = store.get('splitSessionCreation');
65
+ if (!pending || !pending.createdId || !getCachedSessions().some(function (session) { return session.id === pending.createdId; })) return;
66
+ store.set({ splitSessionCreation: null });
67
+ if (!insertPickerSession(pending.receipt, pending.side, pending.createdId)) showToast('The session was created in Unfiled. The View changed, so it was not added to the split.', 'warn');
68
+ }
69
+ store.subscribe(function (state, previous) {
70
+ if (state.splitSessionCreation && (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration || !state.connected)) store.set({ splitSessionCreation: null });
71
+ });
@@ -0,0 +1,81 @@
1
+ import { store } from './store.js';
2
+ import { getCachedSessions } from './sidebar-sessions.js';
3
+ import { activeSavedView } from './view-workspace-state.js';
4
+ import { viewForOrigin } from './split-view-presentation-lookup.js';
5
+ import { createOptions } from './session-create.js';
6
+ import { sortVendorEntries } from './vendor-priority.js';
7
+ import { VENDOR_AVATARS, VENDOR_NAMES } from './chat-render-runtime.js';
8
+ import { iconHtml, refreshIcons } from './icons.js';
9
+ import { pickerCapacity } from './split-view-picker-actions.js';
10
+
11
+ export var SPLIT_SIDES = [['right', 'Right', 'panel-right-open'], ['left', 'Left', 'panel-left-open'], ['top', 'Top', 'panel-top-open'], ['bottom', 'Bottom', 'panel-bottom-open']];
12
+ function button(label, action, value) {
13
+ var el = document.createElement('button'); el.type = 'button'; el.dataset[action] = value; el.setAttribute('aria-label', label); el.title = label; return el;
14
+ }
15
+ function vendorIcon(vendor) {
16
+ var image = document.createElement('img'); image.src = VENDOR_AVATARS[vendor] || '/icon-192.png'; image.alt = ''; return image;
17
+ }
18
+ function age(time) {
19
+ var minutes = Math.max(0, Math.floor((Date.now() - (time || 0)) / 60000));
20
+ return minutes < 1 ? 'Now' : minutes < 60 ? minutes + 'm' : minutes < 1440 ? Math.floor(minutes / 60) + 'h' : Math.floor(minutes / 1440) + 'd';
21
+ }
22
+ export function pickerCandidates(query) {
23
+ var view = activeSavedView(), text = (query || '').trim().toLocaleLowerCase();
24
+ return (getCachedSessions() || []).filter(function (session) {
25
+ return view && session.sessionOriginId && !session.hidden && session.sessionRole !== 'worker' && !session.isMate && !session.isTui &&
26
+ (session.ownerId || null) === (store.get('myUserId') || null) && !view.sessions.some(function (ref) { return ref.sessionOriginId === session.sessionOriginId; }) &&
27
+ (!text || (session.title || 'New session').toLocaleLowerCase().indexOf(text) >= 0);
28
+ }).sort(function (a, b) { return (b.lastActivity || b.createdAt || 0) - (a.lastActivity || a.createdAt || 0); }).slice(0, text ? 40 : 6);
29
+ }
30
+ export function renderPickerResults(menu) {
31
+ var state = store.get('splitSessionPicker'); if (!state) return;
32
+ var list = menu.querySelector('.split-session-results'); if (!list) return;
33
+ var focused = menu.contains(document.activeElement) && document.activeElement.dataset, focusKey = focused && (focused.splitSession ? ['splitSession', focused.splitSession] : focused.splitVendor ? ['splitVendor', focused.splitVendor] : null);
34
+ list.replaceChildren();
35
+ var candidates = pickerCandidates(state.query), blocked = !pickerCapacity() || !!store.get('projectSplitViewPending') || !!store.get('splitSessionCreation');
36
+ candidates.forEach(function (session) {
37
+ var title = session.title || 'New session', owner = viewForOrigin(session.sessionOriginId);
38
+ var choice = button('Add ' + title, 'splitSession', String(session.id)); choice.className = 'split-session-result'; choice.disabled = blocked;
39
+ choice.appendChild(vendorIcon(session.vendor));
40
+ var content = document.createElement('span'); content.className = 'split-session-result-title'; content.textContent = title; choice.appendChild(content);
41
+ var time = document.createElement('small'); time.textContent = age(session.lastActivity || session.createdAt); choice.appendChild(time);
42
+ choice.title = title + (owner ? ' · Move from ' + owner.name : '');
43
+ if (owner && owner.sessions.length > 1) { var note = document.createElement('small'); note.className = 'split-session-move'; note.textContent = 'Move from ' + owner.name; content.appendChild(note); }
44
+ list.appendChild(choice);
45
+ });
46
+ if (!candidates.length) { var empty = document.createElement('p'); empty.className = 'split-session-empty'; empty.textContent = state.query ? 'No matching sessions. Start a new one below.' : 'No other sessions yet. Start a new one below.'; list.appendChild(empty); }
47
+ var vendors = menu.querySelector('.split-session-vendors'); vendors.replaceChildren();
48
+ var options = createOptions(), installed = sortVendorEntries(options.vendors.filter(function (vendor) { return vendor.installed; }));
49
+ installed.forEach(function (vendor) {
50
+ var label = 'New ' + (VENDOR_NAMES[vendor.id] || vendor.displayName || vendor.id) + ' session';
51
+ var control = button(label, 'splitVendor', vendor.id); control.disabled = blocked; control.appendChild(vendorIcon(vendor.id)); vendors.appendChild(control);
52
+ });
53
+ if (options.error || !installed.length) {
54
+ var hint = document.createElement('span'); hint.className = 'split-session-empty'; hint.textContent = options.error || (options.loaded ? 'No installed vendors available.' : 'Loading vendors…'); vendors.appendChild(hint);
55
+ if (options.error) { var retry = button('Retry loading vendors', 'splitRetry', 'true'); retry.textContent = 'Retry'; vendors.appendChild(retry); }
56
+ }
57
+ var status = menu.querySelector('[role="status"]');
58
+ if (focusKey) { var restored = Array.from(menu.querySelectorAll('button')).find(function (item) { return item.dataset[focusKey[0]] === focusKey[1] && !item.disabled; }); if (restored) restored.focus({ preventScroll: true }); }
59
+ status.textContent = !pickerCapacity() ? 'This View already has eight session panes.' : store.get('projectSplitViewPending') ? 'Saving the View…' : store.get('splitSessionCreation') ? 'Creating session…' : state.error || '';
60
+ }
61
+ export function renderPicker(menu) {
62
+ var state = store.get('splitSessionPicker'); if (!state) return;
63
+ menu.replaceChildren(); menu.dataset.stage = state.side ? 'sessions' : 'direction';
64
+ if (!state.side) {
65
+ var header = document.createElement('div'); header.className = 'split-view-arrange-header';
66
+ var title = document.createElement('strong'); title.textContent = 'Add split'; header.appendChild(title);
67
+ var close = button('Close split picker', 'splitClose', 'true'); close.innerHTML = iconHtml('x'); header.appendChild(close); menu.appendChild(header);
68
+ var directions = document.createElement('div'); directions.className = 'split-session-directions';
69
+ SPLIT_SIDES.forEach(function (side) { var choice = button('Add ' + side[1] + ' Split', 'splitSide', side[0]); choice.disabled = !pickerCapacity() || !!store.get('projectSplitViewPending'); choice.innerHTML = iconHtml(side[2]); var label = document.createElement('span'); label.textContent = 'Add ' + side[1] + ' Split'; choice.appendChild(label); directions.appendChild(choice); });
70
+ menu.appendChild(directions); refreshIcons(); return;
71
+ }
72
+ var search = document.createElement('label'); search.className = 'split-session-search'; search.innerHTML = iconHtml('search');
73
+ var input = document.createElement('input'); input.type = 'search'; input.placeholder = 'Search sessions…'; input.setAttribute('aria-label', 'Search sessions'); input.value = state.query || ''; search.appendChild(input); menu.appendChild(search);
74
+ var recent = document.createElement('div'); recent.className = 'split-session-section'; recent.textContent = state.query ? 'Search results' : 'Recent sessions'; menu.appendChild(recent);
75
+ var list = document.createElement('div'); list.className = 'split-session-results'; list.setAttribute('aria-label', 'Available sessions'); menu.appendChild(list);
76
+ var heading = document.createElement('div'); heading.className = 'split-session-section split-session-new'; heading.textContent = 'New session'; menu.appendChild(heading);
77
+ var vendors = document.createElement('div'); vendors.className = 'split-session-vendors'; vendors.setAttribute('aria-label', 'Installed vendors'); menu.appendChild(vendors);
78
+ var status = document.createElement('p'); status.className = 'split-session-status'; status.setAttribute('role', 'status'); menu.appendChild(status);
79
+ var footer = document.createElement('small'); footer.className = 'split-session-hint'; footer.textContent = '↑↓ Choose · Enter Open · Esc Cancel'; menu.appendChild(footer);
80
+ renderPickerResults(menu); refreshIcons();
81
+ }
@@ -1,7 +1,7 @@
1
1
  import { paintLegacyToolPanes, clearToolPaneWindows } from './tool-pane-legacy.js';
2
2
  import { toolPaneLayoutTree, paintToolPaneWindows, bindWorkspaceDivider, dockedToolPanes } from './tool-pane-layout.js';
3
3
  import { afterPaneMotion } from './split-view-drag-preview.js';
4
- import { layoutScope, currentLayoutScope, presentationTree } from './split-view-layout-scope.js';
4
+ import { layoutScope, currentLayoutScope, presentationTree, bindingMatches } from './split-view-layout-scope.js';
5
5
  // Flat, stable pane presentation for persisted binary trees. Geometry is
6
6
  // applied to existing direct-child .split-pane nodes; iframes are never moved
7
7
  // below a nested layout wrapper.
@@ -17,6 +17,7 @@ import { syncPaneHeaderState } from './split-pane-header-state.js';
17
17
  import { rectangles, resize, validateTree, listIds } from './split-view-layout-client.js';
18
18
  import { iconHtml, refreshIcons } from './icons.js';
19
19
  import { appendLayoutOption } from './split-view-layout-icons.js';
20
+ import { reserveEmptySplit, clearEmptySplit } from './split-session-empty-pane.js';
20
21
 
21
22
  var host = null;
22
23
  var app = null;
@@ -24,7 +25,6 @@ var menuTrigger = null;
24
25
  var resizeObserver = null;
25
26
  var geometryToken = 0;
26
27
  var stickyStatusOrigins = null;
27
-
28
28
  function relocateStickyStatus(enabled) {
29
29
  var ids = ['todo-sticky', 'ralph-sticky', 'debate-sticky'];
30
30
  var main = document.getElementById('main-panels'), appElement = document.getElementById('app');
@@ -52,6 +52,7 @@ export function initPresentationRenderer(hostElement) {
52
52
  host = hostElement;
53
53
  app = document.getElementById("app");
54
54
  ensureArrangeMenu();
55
+ document.addEventListener('clay-split-picker-layout', renderCurrentGeometry);
55
56
  document.addEventListener("click", onClick, true);
56
57
  document.addEventListener("keydown", onKeyDown, true);
57
58
  document.addEventListener("pointermove", onPointerMove, true);
@@ -69,6 +70,7 @@ export function initPresentationRenderer(hostElement) {
69
70
 
70
71
  export function hidePresentationView() {
71
72
  if (!host) return;
73
+ clearEmptySplit(host);
72
74
  var legacySplit = store.get('splitPanes');
73
75
  if (legacySplit) releaseRetainedPanes(host);
74
76
  else clearRetainedPanes(host);
@@ -218,11 +220,12 @@ function applyGeometry(view, tree, stillCurrent) {
218
220
  var windowTree = toolPaneLayoutTree(view, tree);
219
221
  var geometry = rectangles(windowTree, Math.max(0, host.clientWidth - left - right), Math.max(0, host.clientHeight - top - bottom), 7);
220
222
  paintToolPaneWindows(host, view, tree, windowTree, geometry, { left: left, top: top });
223
+ var reserving = reserveEmptySplit(host, geometry, { left: left, top: top });
221
224
  var focused = store.get('projectSplitViewFocusedOrigin');
222
225
  var panes = host.querySelectorAll(':scope > .split-pane');
223
226
  for (var i = 0; i < panes.length; i++) {
224
227
  var frame = panes[i].querySelector('.split-pane-frame'), origin = panes[i].dataset.presentationOrigin || frame && frame.dataset.sessionOriginId;
225
- var rect = host.dataset.singlePane === 'true' && panes.length === 1 && !dockedToolPanes().length ?
228
+ var rect = !reserving && host.dataset.singlePane === 'true' && panes.length === 1 && !dockedToolPanes().length ?
226
229
  { x: 0, y: 0, width: Math.max(0, host.clientWidth - left - right), height: Math.max(0, host.clientHeight - top - bottom) } : geometry.panes[origin];
227
230
  if (!rect) continue;
228
231
  if (focused && geometry.panes[focused]) {
@@ -245,6 +248,7 @@ function applyGeometry(view, tree, stillCurrent) {
245
248
  divider.setAttribute("role", "separator"); divider.setAttribute("tabindex", "0");
246
249
  divider.setAttribute("aria-orientation", entry.axis === "horizontal" ? "vertical" : "horizontal");
247
250
  divider.setAttribute("aria-label", "Resize Split view panels");
251
+ if (reserving) { divider.inert = true; divider.tabIndex = -1; divider.setAttribute('aria-disabled', 'true'); }
248
252
  divider.style.left = (left + entry.rect.x) + "px"; divider.style.top = (top + entry.rect.y) + "px";
249
253
  divider.style.width = entry.rect.width + "px"; divider.style.height = entry.rect.height + "px";
250
254
  if (dockedToolPanes().length) bindWorkspaceDivider(divider, entry, windowTree);
@@ -261,15 +265,11 @@ function applyGeometry(view, tree, stillCurrent) {
261
265
  });
262
266
  });
263
267
  }
264
- function bindingMatches(left, right) {
265
- return !!left && !!right && left.sessionId === right.sessionId && left.sessionOriginId === right.sessionOriginId &&
266
- left.generation === right.generation && left.requestId === right.requestId;
267
- }
268
268
  export function ensureArrangeTrigger(header) {
269
269
  if (!header) return;
270
270
  if (!header.querySelector('[data-action="arrange"]')) {
271
271
  var trigger = document.createElement("button"); trigger.type = "button"; trigger.className = "split-view-arrange-trigger";
272
- trigger.dataset.action = "arrange"; trigger.setAttribute("aria-label", "Arrange sessions");
272
+ trigger.dataset.action = "arrange"; trigger.setAttribute("aria-label", "Split view options");
273
273
  trigger.setAttribute("aria-expanded", "false"); trigger.setAttribute("aria-controls", "split-view-arrange-menu");
274
274
  trigger.innerHTML = iconHtml("columns-2");
275
275
  var close = header.querySelector(".split-pane-close"); header.insertBefore(trigger, close || null);
@@ -22,14 +22,14 @@ export function workerActivityLabel(message, names) {
22
22
  }
23
23
  export function captureWorkerDisclosures(body) {
24
24
  return {
25
- open: Array.from(body.querySelectorAll('details[open]')).map(function (node) { return node.dataset.disclosureKey; }),
25
+ open: Array.from(body.querySelectorAll('details[open], [data-disclosure-key][aria-expanded="true"]')).map(function (node) { return node.dataset.disclosureKey; }),
26
26
  focus: document.activeElement && body.contains(document.activeElement) && (document.activeElement.dataset.disclosureKey || document.activeElement.parentElement && document.activeElement.parentElement.dataset.disclosureKey)
27
27
  };
28
28
  }
29
29
  export function restoreWorkerDisclosures(body, saved) {
30
30
  Array.from(body.querySelectorAll('[data-disclosure-key]')).forEach(function (node) {
31
31
  var key = node.dataset.disclosureKey;
32
- if (saved.open.indexOf(key) >= 0) node.open = true;
32
+ if (saved.open.indexOf(key) >= 0) { if (node.tagName === 'DETAILS') node.open = true; else if (node.getAttribute('aria-expanded') !== 'true') node.click(); }
33
33
  if (saved.focus === key) {
34
34
  var target = node.tagName === 'DETAILS' ? node.querySelector('summary') : node;
35
35
  if (target) target.focus({ preventScroll: true });
@@ -1,5 +1,9 @@
1
- import { workerActivityLabel, workerActivityNames } from './split-view-worker-activity.js';
2
- import { renderMarkdown } from './markdown.js';
1
+ import { workerActivityNames } from './split-view-worker-activity.js';
2
+ import { createAssistantBubble, createUserBubble, renderAssistantBubbleText } from './chat-bubble-renderer.js';
3
+ import { createSessionToolItem, createSessionToolGroup, sessionToolGroupSummary } from './session-tool-item.js';
4
+ import { applyClayToolItem, bindToolGroupDisclosure } from './clay-tool-activity.js';
5
+ import { iconHtml } from './icons.js';
6
+ import { VENDOR_AVATARS } from './chat-render-runtime.js';
3
7
 
4
8
  // Reassemble bounded record fragments and contiguous streaming deltas before
5
9
  // parsing Markdown, including fences and lists that cross a page boundary.
@@ -17,38 +21,71 @@ export function workerTranscriptRows(messages) {
17
21
  return rows;
18
22
  }
19
23
 
20
- export function appendWorkerMessage(body, message, names) {
24
+ function inputFor(message) {
25
+ if (message.recordType !== 'tool_use' && message.recordType !== 'tool_executing') return null;
26
+ try { var start = message.text.indexOf('{'); return start >= 0 ? JSON.parse(message.text.slice(start)) : null; } catch (error) { return null; }
27
+ }
28
+ export function appendWorkerMessage(body, message, names, options) {
29
+ options = options || {};
21
30
  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;
31
+ var name = message.toolName || names && names[message.toolId] && names[message.toolId].name || 'Tool activity';
32
+ var el = createSessionToolItem(message.toolId, name);
33
+ el.classList.add('worker-readonly-message', 'worker-tool-message'); el.dataset.recordIndex = String(message.recordIndex);
34
+ el.dataset.phase = message.isError ? 'failed' : message.recordType === 'tool_result' ? 'returned' : 'recorded';
35
+ var input = inputFor(message), output = message.resultText;
36
+ var tool = { el: el, name: name, input: input, clayRawResult: output, clayOutcome: output !== undefined ? { phase: message.isError ? 'error' : 'done' } : { phase: 'returned', status: 'result_unavailable' } };
37
+ var header = el.querySelector('.tool-header'); header.dataset.disclosureKey = 'record-' + message.recordIndex;
38
+ if (!applyClayToolItem(tool)) {
39
+ var terminal = message.isError ? 'Failed' : output !== undefined || message.recordType === 'tool_result' ? 'Result returned' : 'Call recorded';
40
+ el.querySelector('.tool-desc').textContent = input && (input.file_path || input.command || input.pattern) || '';
41
+ el.querySelector('.tool-subtitle-text').textContent = terminal;
42
+ el.querySelector('.tool-status-icon').innerHTML = iconHtml(message.isError ? 'alert-triangle' : output !== undefined ? 'check' : 'circle-help');
43
+ el.classList.toggle('done', output !== undefined); el.classList.toggle('error', !!message.isError);
44
+ var block = document.createElement('div'); block.className = 'tool-result-block collapsed';
45
+ var pre = document.createElement('pre'); pre.textContent = output !== undefined ? output : message.text; block.appendChild(pre); el.appendChild(block);
46
+ header.setAttribute('role', 'button'); header.tabIndex = 0; header.setAttribute('aria-expanded', 'false');
47
+ function toggle() { var expanded = el.classList.toggle('expanded'); block.classList.toggle('collapsed', !expanded); header.setAttribute('aria-expanded', String(expanded)); }
48
+ header.addEventListener('click', toggle); header.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggle(); } });
49
+ }
50
+ body.appendChild(el); return;
34
51
  }
35
- var article = document.createElement('article');
36
- article.className = 'message worker-readonly-message ' + (message.role === 'user' ? 'msg-user' : 'msg-assistant');
37
- if (Number.isInteger(message.recordIndex)) article.dataset.recordIndex = String(message.recordIndex);
38
- var author = document.createElement('strong'); author.className = 'author';
39
- author.textContent = message.role === 'user' ? 'Driver' : 'Worker';
40
- var content = document.createElement('div'); content.className = 'message-body md-content';
41
- // Recorded HTML must not create controls, scoped overlays or active embeds.
42
- // Keep the shared Markdown parser and its sanitization, with a narrower
43
- // read-only prose vocabulary for this secondary history surface.
44
- content.innerHTML = DOMPurify.sanitize(renderMarkdown(message.text), {
45
- ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'del', 's', 'blockquote', 'ul', 'ol', 'li', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'a', 'wbr'],
46
- ALLOWED_ATTR: ['href', 'title', 'target', 'rel', 'start'], ALLOW_DATA_ATTR: false
47
- });
48
- article.append(author, content); body.appendChild(article);
52
+ var user = message.role === 'user';
53
+ var label = message.authorLabel || (user ? options.userName || 'Driver' : options.assistantName || 'Worker');
54
+ var config = { tagName: 'article', name: label, avatarUrl: user ? options.userAvatar || '/icon-192.png' : VENDOR_AVATARS[options.vendor || 'claude'], text: message.text };
55
+ var article = user ? createUserBubble(config) : createAssistantBubble(config);
56
+ article.classList.add('worker-readonly-message'); article.dataset.recordIndex = String(message.recordIndex);
57
+ if (!user) renderAssistantBubbleText(article, message.text, true, { readOnly: true });
58
+ body.appendChild(article);
49
59
  }
50
-
51
- export function appendWorkerTranscript(body, messages) {
52
- var rows = workerTranscriptRows(messages), names = workerActivityNames(rows);
53
- rows.forEach(function (message) { appendWorkerMessage(body, message, names); });
60
+ export function appendWorkerTranscript(body, messages, options) {
61
+ var rows = workerTranscriptRows(messages), names = workerActivityNames(rows), consumed = new Set(), visible = [];
62
+ rows.forEach(function (message, index) {
63
+ if (consumed.has(index)) return;
64
+ var row = message;
65
+ if (message.role === 'tool' && names[message.toolId] && names[message.toolId].index === message.recordIndex) {
66
+ var later = rows.slice(index + 1), executing = later.find(function (item) { return item.toolId === message.toolId && item.recordType === 'tool_executing'; });
67
+ var result = later.find(function (item) { return item.toolId === message.toolId && item.recordType === 'tool_result'; });
68
+ row = Object.assign({}, message, executing ? { text: executing.text, recordType: executing.recordType } : {}, result ? { resultText: result.text, isError: result.isError } : {});
69
+ [executing, result].forEach(function (item) { if (item) consumed.add(rows.indexOf(item)); });
70
+ }
71
+ visible.push(row);
72
+ });
73
+ var group = null, groupRows = [];
74
+ function finish() {
75
+ if (!group) return;
76
+ var header = group.querySelector('.tool-group-header'), done = groupRows.every(function (row) { return row.resultText !== undefined || row.recordType === 'tool_result'; });
77
+ var meta = { toolNames: groupRows.map(function (row) { return row.toolName || names[row.toolId] && names[row.toolId].name || 'Tool'; }), doneCount: done ? groupRows.length : 0 };
78
+ header.style.display = groupRows.length > 1 ? '' : 'none';
79
+ header.dataset.disclosureKey = 'group-' + groupRows[0].recordIndex;
80
+ group.querySelector('.tool-group-label').textContent = done ? sessionToolGroupSummary(meta, {}) : groupRows.length + ' tool calls recorded';
81
+ group.classList.toggle('done', done); group.classList.toggle('collapsed', groupRows.length > 1);
82
+ group.querySelector('.tool-group-status-icon').innerHTML = iconHtml(done ? 'check' : 'circle-help');
83
+ bindToolGroupDisclosure(group, function () {}); group = null; groupRows = [];
84
+ }
85
+ visible.forEach(function (row) {
86
+ if (row.role !== 'tool') { finish(); appendWorkerMessage(body, row, names, options); return; }
87
+ if (!group) { group = createSessionToolGroup(); body.appendChild(group); }
88
+ groupRows.push(row); appendWorkerMessage(group.querySelector('.tool-group-items'), row, names, options);
89
+ });
90
+ finish();
54
91
  }
@@ -1,3 +1,4 @@
1
+ import { syncWorkerMessagePreviews } from './worker-message-previews.js';
1
2
  import { appendWorkerRemoval, reconcileWorkerRemoval } from './split-view-worker-removal.js';
2
3
  import { workerCursorKey, workerCursorAdvances, shouldLoadWorkerPage, captureWorkerAnchor, restoreWorkerAnchor } from './split-view-worker-scroll.js';
3
4
  import { appendWorkerTranscript } from './split-view-worker-transcript.js';
@@ -189,7 +190,7 @@ function open(chip, driver, worker, takeFocus) {
189
190
  if (existing && existing.driverOriginId === driver.sessionOriginId && existing.workerOriginId === worker.sessionOriginId && existing.generation === worker.workerGeneration) { close(true); return; }
190
191
  if (existing) close(false);
191
192
  var state = { requestId: null, driverOriginId: driver.sessionOriginId, workerOriginId: worker.sessionOriginId,
192
- generation: worker.workerGeneration, driverTitle: driver.title || "Driver session", workerTitle: worker.title || "Worker",
193
+ generation: worker.workerGeneration, workerVendor: worker.vendor, driverTitle: driver.title || "Driver session", workerTitle: worker.title || "Worker",
193
194
  sourceBinding: chip.closest('.split-pane').querySelector('.split-pane-frame').clayPane.identity().binding,
194
195
  status: "loading", messages: [], hasMore: false, nextBefore: null, append: false, inFlight: false, initialPage: true };
195
196
  store.set({ splitViewWorkerConversation: state }); ensurePopover(); renderContent();
@@ -217,7 +218,7 @@ function ensurePopover() {
217
218
  var closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.setAttribute("aria-label", "Close Worker conversation"); closeButton.innerHTML = iconHtml("x");
218
219
  closeButton.addEventListener("click", function () { close(true); }); header.appendChild(closeButton); element.appendChild(header);
219
220
  var source = document.createElement("div"); source.className = "split-view-worker-popover-source"; element.appendChild(source);
220
- var body = document.createElement("div"); body.className = "split-view-worker-popover-body"; body.tabIndex = 0; body.setAttribute("aria-label", "Read-only Worker transcript"); element.appendChild(body);
221
+ var body = document.createElement("div"); body.className = "split-view-worker-popover-body readonly-session-transcript"; body.tabIndex = 0; body.setAttribute("aria-label", "Read-only Worker transcript"); element.appendChild(body);
221
222
  body.addEventListener('scroll', function () { loadEarlier(body, true); }, { passive: true });
222
223
  body.addEventListener('wheel', function (event) { if (event.deltaY < 0) loadEarlier(body, true); }, { passive: true });
223
224
  var footer = document.createElement("footer"); footer.className = "split-view-worker-popover-footer"; element.appendChild(footer);
@@ -241,7 +242,7 @@ function renderContent() {
241
242
  if (state.status === "loading" && !state.messages.length) body.textContent = "Loading conversation…";
242
243
  else if (state.status === "error" && !state.messages.length) body.textContent = state.error || "Conversation is unavailable.";
243
244
  else if (!state.messages.length) body.textContent = "No recorded conversation.";
244
- appendWorkerTranscript(body, state.messages);
245
+ appendWorkerTranscript(body, state.messages, { vendor: state.workerVendor, assistantName: state.workerTitle });
245
246
  restoreWorkerDisclosures(body, disclosures);
246
247
  if ((state.initialPage || state.liveRefresh && followTail) && state.status === 'ready') body.scrollTop = body.scrollHeight;
247
248
  else if (anchor) restoreWorkerAnchor(body, anchor);
@@ -370,7 +371,7 @@ export function renderPaneWorkers(container, driver) {
370
371
  data.items.forEach(function (worker) {
371
372
  var pill = document.createElement("span"); pill.className = "split-view-worker-pill";
372
373
  var button = document.createElement("button"); button.type = "button"; button.className = "split-view-worker-chip";
373
- button.dataset.workerOrigin = worker.sessionOriginId; button.dataset.workerGeneration = String(worker.workerGeneration);
374
+ button.dataset.workerParent = driver.sessionOriginId; button.dataset.workerOrigin = worker.sessionOriginId; button.dataset.workerGeneration = String(worker.workerGeneration);
374
375
  var current = data.currentIds.has(worker.id);
375
376
  var modelLabel = worker.model || "Default model";
376
377
  button.title = (VENDOR_NAMES[worker.vendor] || worker.vendor || "Unknown vendor") + " · " + modelLabel + " · Split Worker #" + worker.id +
@@ -419,6 +420,7 @@ export function renderPaneWorkers(container, driver) {
419
420
  root.appendChild(controls);
420
421
  }
421
422
  introduceWorkerViewer(container, driver);
423
+ syncWorkerMessagePreviews();
422
424
  var conversation = store.get('splitViewWorkerConversation');
423
425
  if (conversation) ensurePopover();
424
426
  }