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

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 (154) hide show
  1. package/lib/clay-control-presentation-binding.js +5 -3
  2. package/lib/clay-product-control-tools.js +3 -3
  3. package/lib/clay-product-control.js +4 -2
  4. package/lib/clay-screen-controls.js +18 -3
  5. package/lib/clay-split-view-awareness.js +35 -0
  6. package/lib/clay-split-view-controls.js +122 -0
  7. package/lib/clay-split-view-input.js +20 -0
  8. package/lib/clay-split-view-render-proof.js +22 -0
  9. package/lib/pane-asset-manifest.js +63 -0
  10. package/lib/project-connection.js +19 -10
  11. package/lib/project-session-actions.js +1 -0
  12. package/lib/project-session-delete.js +2 -2
  13. package/lib/project-session-folders.js +1 -0
  14. package/lib/project-sessions.js +6 -1
  15. package/lib/project-split-view-document.js +33 -0
  16. package/lib/project-split-view-names.js +67 -0
  17. package/lib/project-split-view-organization.js +23 -0
  18. package/lib/project-split-views.js +479 -0
  19. package/lib/project-worker-conversation.js +222 -0
  20. package/lib/project-worker-creation-proposal.js +3 -4
  21. package/lib/project-worker-proposal.js +1 -1
  22. package/lib/project-worker-removal.js +101 -0
  23. package/lib/project.js +28 -1
  24. package/lib/public/app.js +32 -13
  25. package/lib/public/css/base.css +9 -0
  26. package/lib/public/css/filebrowser.css +0 -1
  27. package/lib/public/css/pane-header-controls.css +100 -0
  28. package/lib/public/css/pane.css +24 -41
  29. package/lib/public/css/popup-chat.css +123 -0
  30. package/lib/public/css/right-workbench.css +50 -18
  31. package/lib/public/css/session-folders.css +1 -1
  32. package/lib/public/css/split-view-layout.css +463 -0
  33. package/lib/public/css/split-view-worker-activity.css +97 -0
  34. package/lib/public/css/sticky-notes.css +13 -0
  35. package/lib/public/index.html +3 -3
  36. package/lib/public/modules/app-connection.js +4 -1
  37. package/lib/public/modules/app-favicon.js +1 -1
  38. package/lib/public/modules/app-messages.js +31 -9
  39. package/lib/public/modules/chat-render-runtime.js +54 -0
  40. package/lib/public/modules/clay-control-copy.js +3 -0
  41. package/lib/public/modules/clay-navigation-render.js +1 -1
  42. package/lib/public/modules/clay-owner-navigation.js +15 -5
  43. package/lib/public/modules/clay-split-view-screen.js +74 -0
  44. package/lib/public/modules/clay-tool-activity.js +5 -0
  45. package/lib/public/modules/command-palette.js +30 -28
  46. package/lib/public/modules/contextual-clay-session.js +26 -2
  47. package/lib/public/modules/contextual-clay.js +5 -2
  48. package/lib/public/modules/contextual-mate-sessions.js +2 -0
  49. package/lib/public/modules/filebrowser.js +6 -2
  50. package/lib/public/modules/generation-stop.js +7 -1
  51. package/lib/public/modules/knowledge-workbench-entry.js +43 -0
  52. package/lib/public/modules/markdown-view-mode.js +34 -0
  53. package/lib/public/modules/markdown.js +11 -0
  54. package/lib/public/modules/mate-knowledge-workbench.js +2 -7
  55. package/lib/public/modules/mate-sidebar.js +1 -1
  56. package/lib/public/modules/palette-clay-conversation.js +87 -0
  57. package/lib/public/modules/palette-render-helpers.js +25 -0
  58. package/lib/public/modules/pane-activation.js +46 -7
  59. package/lib/public/modules/pane-binding.js +12 -2
  60. package/lib/public/modules/pane-bridge.js +12 -3
  61. package/lib/public/modules/pane-deferred-bootstrap.js +32 -0
  62. package/lib/public/modules/pane-feature-assets.js +35 -0
  63. package/lib/public/modules/pane-file-bridge.js +8 -3
  64. package/lib/public/modules/pane-search-receipt.js +12 -0
  65. package/lib/public/modules/permission-control.js +6 -6
  66. package/lib/public/modules/popup-chat-rendering.js +244 -0
  67. package/lib/public/modules/project-activation.js +2 -2
  68. package/lib/public/modules/qrcode.js +12 -1
  69. package/lib/public/modules/right-workbench.js +68 -0
  70. package/lib/public/modules/search-hints.js +79 -0
  71. package/lib/public/modules/session-context-menu.js +35 -2
  72. package/lib/public/modules/session-folder-collapse.js +2 -1
  73. package/lib/public/modules/session-folder-context.js +24 -13
  74. package/lib/public/modules/session-folder-delete.js +18 -1
  75. package/lib/public/modules/session-folder-dnd.js +4 -1
  76. package/lib/public/modules/session-folder-layout.js +7 -6
  77. package/lib/public/modules/session-search-pane-state.js +30 -0
  78. package/lib/public/modules/session-search.js +53 -13
  79. package/lib/public/modules/sidebar-mobile.js +3 -3
  80. package/lib/public/modules/sidebar-projects.js +1 -0
  81. package/lib/public/modules/sidebar-session-folders-ui.js +3 -3
  82. package/lib/public/modules/sidebar-sessions.js +57 -24
  83. package/lib/public/modules/sidebar.js +20 -3
  84. package/lib/public/modules/split-group-helpers.js +1 -1
  85. package/lib/public/modules/split-group-restore.js +8 -11
  86. package/lib/public/modules/split-pair-ui.js +18 -0
  87. package/lib/public/modules/split-pane-header-state.js +45 -0
  88. package/lib/public/modules/split-pane-reconciler.js +23 -4
  89. package/lib/public/modules/split-pane-renderer.js +330 -23
  90. package/lib/public/modules/split-pane-retention.js +157 -0
  91. package/lib/public/modules/split-session-boundary.js +21 -0
  92. package/lib/public/modules/split-view-create-action.js +15 -0
  93. package/lib/public/modules/split-view-create-menu.js +83 -0
  94. package/lib/public/modules/split-view-create-request.js +23 -0
  95. package/lib/public/modules/split-view-layout-client.js +198 -0
  96. package/lib/public/modules/split-view-layout-icons.js +37 -0
  97. package/lib/public/modules/split-view-layout-scope.js +38 -0
  98. package/lib/public/modules/split-view-organization.js +73 -0
  99. package/lib/public/modules/split-view-presentation-close.js +35 -0
  100. package/lib/public/modules/split-view-presentation-drag.js +278 -0
  101. package/lib/public/modules/split-view-presentation-drop.js +57 -0
  102. package/lib/public/modules/split-view-presentation-lookup.js +44 -0
  103. package/lib/public/modules/split-view-presentation-renderer.js +494 -0
  104. package/lib/public/modules/split-view-presentations.js +498 -0
  105. package/lib/public/modules/split-view-shell-controls.js +28 -0
  106. package/lib/public/modules/split-view-sidebar-menu.js +42 -0
  107. package/lib/public/modules/split-view-sidebar-renderer.js +30 -0
  108. package/lib/public/modules/split-view-singleton.js +21 -0
  109. package/lib/public/modules/split-view-worker-activity.js +94 -0
  110. package/lib/public/modules/split-view-worker-controls.js +30 -0
  111. package/lib/public/modules/split-view-worker-removal.js +137 -0
  112. package/lib/public/modules/split-view-worker-scroll.js +57 -0
  113. package/lib/public/modules/split-view-worker-transcript.js +46 -0
  114. package/lib/public/modules/split-view-workers.js +418 -0
  115. package/lib/public/modules/split-view.js +61 -22
  116. package/lib/public/modules/sticky-notes.js +9 -2
  117. package/lib/public/modules/tools.js +25 -5
  118. package/lib/public/modules/workbench-layout-preference.js +76 -0
  119. package/lib/public/modules/worker-created-presentation.js +37 -0
  120. package/lib/public/pane-assets/app-2DRBYY3L.js +1 -0
  121. package/lib/public/pane-assets/chunk-4BM7UVNG.js +2 -0
  122. package/lib/public/pane-assets/chunk-6SM2WQJX.js +1 -0
  123. package/lib/public/pane-assets/chunk-7H36SUZA.js +2 -0
  124. package/lib/public/pane-assets/chunk-C4RVVYNT.js +1 -0
  125. package/lib/public/pane-assets/chunk-DGOFGEUK.js +1 -0
  126. package/lib/public/pane-assets/chunk-I57URYSL.js +1 -0
  127. package/lib/public/pane-assets/chunk-O6MQL6MR.js +167 -0
  128. package/lib/public/pane-assets/chunk-PO2JOHHW.js +1 -0
  129. package/lib/public/pane-assets/chunk-UMGHINOT.js +1 -0
  130. package/lib/public/pane-assets/manifest.json +553 -0
  131. package/lib/public/pane-assets/style-63EZCREE.css +1 -0
  132. package/lib/public/style.css +5 -1
  133. package/lib/server-owner-ui.js +6 -2
  134. package/lib/server-owner-view-observation.js +34 -0
  135. package/lib/server-palette.js +1 -0
  136. package/lib/server-pane-bootstrap.js +24 -0
  137. package/lib/server-settings.js +2 -0
  138. package/lib/server-static-assets.js +52 -0
  139. package/lib/server-workbench-layout-settings.js +56 -0
  140. package/lib/server.js +6 -45
  141. package/lib/session-activity-state.js +28 -0
  142. package/lib/session-list-delivery.js +17 -0
  143. package/lib/session-pair-turn-control.js +1 -6
  144. package/lib/session-search.js +3 -0
  145. package/lib/session-split-group-anchors.js +2 -2
  146. package/lib/session-split-group-roles.js +5 -8
  147. package/lib/session-split-group-v2-store.js +0 -3
  148. package/lib/sessions.js +16 -6
  149. package/lib/split-view-layout.js +132 -0
  150. package/lib/users-workbench-layout-preferences.js +33 -0
  151. package/lib/users.js +3 -0
  152. package/lib/workspace-query-service.js +1 -5
  153. package/lib/ws-schema.js +16 -0
  154. package/package.json +6 -3
@@ -0,0 +1,157 @@
1
+ import { getWs } from './ws-ref.js';
2
+ import { store } from './store.js';
3
+ import { getCachedSessions } from './sidebar-sessions.js';
4
+ import { paneTarget, samePaneTarget } from './pane-binding.js';
5
+
6
+ // DOM resources are local to this parent document, never store data.
7
+ var pools = new Map();
8
+ var clock = 0;
9
+ function scope(state) {
10
+ if (!state.authIdentityLoaded || state.paneMode || state.dmMode || state.activeProjectMateId ||
11
+ !state.currentSlug || /^mate-/.test(state.currentSlug) || state.activeProjectSlug && state.activeProjectSlug !== state.currentSlug ||
12
+ state.sessionListProjectSlug && state.sessionListProjectSlug !== state.currentSlug) return null;
13
+ return JSON.stringify([state.currentSlug, state.currentProjectOwnerId, state.myUserId, state.controlRuntimeId, state.cwd,
14
+ state.isOsUsers, state.paneOsIdentity]);
15
+ }
16
+ function target(state, id) {
17
+ var session = (getCachedSessions() || []).find(function (item) { return item.id === id; });
18
+ if (!session || session.hidden || session.isTui || session.sessionRole === 'worker' || (session.ownerId || null) !== (state.myUserId || null)) return null;
19
+ return paneTarget(state, getCachedSessions(), id);
20
+ }
21
+ // Conversation identity controls teardown; execution-group metadata controls
22
+ // warm reuse and action authority, not the lifetime of a visible Driver DOM.
23
+ function sameConversation(a, b) {
24
+ return !!a && !!b && ['projectSlug', 'viewerId', 'ownerId', 'sessionId', 'sessionOriginId',
25
+ 'role', 'workerGeneration', 'parentSessionOriginId'].every(function (key) { return a[key] === b[key]; });
26
+ }
27
+ function suspend(node) {
28
+ var frame = node.querySelector('.split-pane-frame'), header = node.querySelector('.split-pane-header');
29
+ if (header) { header.inert = true; header.dispatchEvent(new Event('clay-pane-retarget')); }
30
+ if (frame) {
31
+ if (frame.clayPane) frame.clayPane.suspend();
32
+ frame.dispatchEvent(new Event('clay-pane-retarget'));
33
+ }
34
+ }
35
+ function evict(pool, node) {
36
+ pool.entries.delete(node);
37
+ if (node.dispatchEvent) node.dispatchEvent(new Event('clay-pane-dispose'));
38
+ suspend(node); node.remove();
39
+ }
40
+ export function clearRetainedPanes(host) {
41
+ var pool = pools.get(host);
42
+ if (!pool) return;
43
+ Array.from(pool.entries.keys()).forEach(function (node) { evict(pool, node); });
44
+ pools.delete(host);
45
+ }
46
+ export function disposePaneChildren(host) {
47
+ Array.from(host && host.children || []).forEach(function (pane) {
48
+ if (pane.dispatchEvent) pane.dispatchEvent(new Event('clay-pane-dispose'));
49
+ });
50
+ }
51
+ // Leave active nodes attached for the existing legacy reconciler; never hand
52
+ // hidden cached documents to a Worker pair.
53
+ export function releaseRetainedPanes(host) {
54
+ var pool = pools.get(host);
55
+ if (!pool) return;
56
+ Array.from(pool.entries.keys()).forEach(function (node) {
57
+ if (node.classList.contains('split-pane-cached')) evict(pool, node);
58
+ });
59
+ pools.delete(host);
60
+ }
61
+ function invalidateTransport(pool) {
62
+ // Cached documents cannot survive a parent transport boundary. Visible panes
63
+ // have independent authenticated sockets; keep their drafts and Stop control.
64
+ Array.from(pool.entries.keys()).forEach(function (node) {
65
+ if (node.classList.contains('split-pane-cached')) evict(pool, node);
66
+ else { pool.entries.get(node).reuseInvalidated = true; var header = node.querySelector('.split-pane-header'); if (header) header.inert = true; }
67
+ });
68
+ }
69
+ export function preserveReconnectingPresentation(host) {
70
+ var state = store.snap(), pool = pools.get(host);
71
+ if (!pool || scope(state) !== pool.scope || state.splitIdentityBlockedSocket === getWs()) return false;
72
+ if (state.connected && state.activeProjectSlug === state.currentSlug && state.sessionListProjectSlug === state.currentSlug && state.projectSplitViewHydrated) return false;
73
+ return Array.from(pool.entries.keys()).some(function (node) {
74
+ var frame = node.querySelector('.split-pane-frame'), api = frame && frame.clayPane, live = api && api.identity();
75
+ return node.parentNode === host && !node.hidden && !node.classList.contains('split-pane-cached') && api && api.source === frame.contentWindow && api.document === frame.contentDocument &&
76
+ live && live.ready && live.ownerId === state.myUserId && live.projectSlug === state.currentSlug && sameConversation(pool.entries.get(node).target, live.binding);
77
+ });
78
+ }
79
+ function prune(host, pool) {
80
+ var state = store.snap();
81
+ if (scope(state) !== pool.scope) { clearRetainedPanes(host); return false; }
82
+ var generation = state.projectSplitViewSocketGeneration;
83
+ if (getWs() !== pool.socket || generation !== pool.generation || state.connected !== pool.connected) {
84
+ invalidateTransport(pool);
85
+ pool.socket = getWs(); pool.generation = generation; pool.connected = state.connected;
86
+ }
87
+ if (state.splitPanes) { releaseRetainedPanes(host); return false; }
88
+ Array.from(pool.entries.entries()).forEach(function (entry) {
89
+ var node = entry[0], record = entry[1];
90
+ var current = target(state, record.target.sessionId);
91
+ if (node.parentNode !== host || !sameConversation(record.target, current)) { evict(pool, node); return; }
92
+ if (!samePaneTarget(record.target, current)) {
93
+ if (node.classList.contains('split-pane-cached')) evict(pool, node);
94
+ else record.reuseInvalidated = true;
95
+ }
96
+ });
97
+ return true;
98
+ }
99
+ export function paneRetention(host) {
100
+ var key = scope(store.snap()), pool = pools.get(host);
101
+ if (pool && !prune(host, pool)) pool = null;
102
+ if (!key || !store.get('connected') || !getWs() || store.get('splitPanes')) return null;
103
+ if (!pool) { pool = { scope: key, socket: getWs(), generation: store.get('projectSplitViewSocketGeneration'), connected: store.get('connected'), entries: new Map() }; pools.set(host, pool); }
104
+ return {
105
+ limit: 8,
106
+ age: function (node) { return (pool.entries.get(node) || {}).used || 0; },
107
+ remember: function (node, pane) {
108
+ var binding = target(store.snap(), pane.sessionId);
109
+ if (binding) pool.entries.set(node, { target: binding, used: ++clock });
110
+ },
111
+ retain: function (node) {
112
+ var record = pool.entries.get(node), frame = node.querySelector('.split-pane-frame');
113
+ var api = frame && frame.clayPane, live = api && api.identity();
114
+ return !!(record && !record.reuseInvalidated && api && typeof api.suspend === 'function' && api.source === frame.contentWindow && api.document === frame.contentDocument &&
115
+ live && samePaneTarget(record.target, live.binding) && samePaneTarget(record.target, target(store.snap(), record.target.sessionId)) && !frame.dataset.requestedSessionId);
116
+ },
117
+ canRetarget: function (node) {
118
+ if (pool.entries.has(node)) return this.retain(node);
119
+ // Legacy nodes are not pool entries. Validate their actual current
120
+ // document and authoritative source before handing them to activation.
121
+ var state = store.snap(), frame = node.querySelector('.split-pane-frame');
122
+ var api = frame && frame.clayPane, live = api && api.identity();
123
+ var session = live && (getCachedSessions() || []).find(function (item) { return item.id === live.sessionId; });
124
+ return !!(node.parentNode === host && node.classList.contains('split-pane') && !node.hidden &&
125
+ scope(state) === pool.scope && getWs() === pool.socket && session && !session.hidden &&
126
+ (session.ownerId || null) === (state.myUserId || null) && api.source === frame.contentWindow && api.document === frame.contentDocument &&
127
+ samePaneTarget(live.binding, paneTarget(state, getCachedSessions(), live.sessionId)) && !frame.dataset.requestedSessionId);
128
+ },
129
+ park: function (node) {
130
+ if (node.classList.contains('split-pane-cached')) return;
131
+ suspend(node); node.hidden = true; node.inert = true; node.style.display = 'none';
132
+ node.classList.remove('split-pane'); node.classList.add('split-pane-cached');
133
+ },
134
+ restore: function (node) {
135
+ var record = pool.entries.get(node);
136
+ if (record && record.reuseInvalidated) {
137
+ var frame = node.querySelector('.split-pane-frame'), live = frame && frame.clayPane && frame.clayPane.identity();
138
+ if (!live || !live.ready || !samePaneTarget(live.binding, target(store.snap(), record.target.sessionId))) suspend(node);
139
+ }
140
+ node.classList.remove('split-pane-cached'); node.classList.add('split-pane');
141
+ node.hidden = false; node.inert = false; node.style.display = '';
142
+ },
143
+ evict: function (node) { evict(pool, node); },
144
+ };
145
+ }
146
+ store.subscribe(function (state, previous) {
147
+ var removedView = state.projectSplitViews !== previous.projectSplitViews && (previous.projectSplitViews || []).some(function (view) {
148
+ return !(state.projectSplitViews || []).some(function (current) { return current.id === view.id; });
149
+ });
150
+ pools.forEach(function (pool, host) {
151
+ if (!prune(host, pool)) return;
152
+ if (removedView) Array.from(pool.entries.keys()).forEach(function (node) { if (node.classList.contains('split-pane-cached')) evict(pool, node); });
153
+ });
154
+ });
155
+ if (typeof window !== 'undefined' && window.addEventListener) window.addEventListener('pagehide', function () {
156
+ Array.from(pools.keys()).forEach(clearRetainedPanes);
157
+ });
@@ -11,6 +11,27 @@ export function clearProjectSplitState() {
11
11
  splitGroups: [],
12
12
  splitDelegations: {},
13
13
  splitGroupsProjectSlug: null,
14
+ projectSplitViews: [],
15
+ projectSplitViewRevision: 0,
16
+ projectSplitViewContextKey: null,
17
+ projectSplitViewHydrated: false,
18
+ projectSplitViewPending: null,
19
+ projectSplitViewError: "",
20
+ projectSplitViewActiveId: null,
21
+ projectSplitViewFocusedOrigin: null,
22
+ projectSplitViewPreviewTree: null,
23
+ projectSplitViewDrag: null,
24
+ projectSplitViewDividerDrag: null,
25
+ projectSplitViewTranscriptScroll: {},
26
+ paneLayoutScrollLock: null,
27
+ paneLastUserScrollTop: null,
28
+ paneUserScrollIntent: null,
29
+ paneUserScrollRevision: 0,
30
+ projectSplitViewRequestSocket: null,
31
+ projectSplitViewRequestGeneration: null,
32
+ projectSplitViewRequestOwnerId: null,
33
+ projectSplitViewRequestSlug: null,
34
+ projectSplitViewSuppressClick: null,
14
35
  sessionListProjectSlug: null,
15
36
  activeSessionId: null,
16
37
  sessionActivatedProjectSlug: null,
@@ -0,0 +1,15 @@
1
+ import { store } from './store.js';
2
+ import { getCachedSessions } from './sidebar-sessions.js';
3
+ import { checkComposerDeparture } from './session-rich-drafts.js';
4
+ import { showToast } from './utils.js';
5
+ import { buildSplitViewCreateRequest } from './split-view-create-request.js';
6
+
7
+ export function handleSplitViewCreateEvent(event, submit, checkMountedPaneDrafts) {
8
+ var origins = event.detail && event.detail.origins || [], sessions = getCachedSessions() || [];
9
+ var views = store.get('projectSplitViews') || [], activeView = views.find(function (view) { return view.id === store.get('projectSplitViewActiveId'); });
10
+ var prepared = buildSplitViewCreateRequest(origins, sessions, store.get('activeSessionId'), activeView);
11
+ if (!prepared.ok) { showToast(prepared.error, 'warn'); return; }
12
+ try { if (activeView) checkMountedPaneDrafts(); else checkComposerDeparture(); }
13
+ catch (error) { showToast(error.message, 'warn'); return; }
14
+ submit(prepared.type, prepared.body, prepared.meta);
15
+ }
@@ -0,0 +1,83 @@
1
+ // Pane-header picker for explicitly choosing real Driver sessions for a Split view.
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';
6
+
7
+ function menuElement() { return document.getElementById('split-view-create-menu'); }
8
+ function close(returnFocus) {
9
+ 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;
13
+ }
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 + ')';
18
+ }
19
+ function open(anchor) {
20
+ var menu = menuElement();
21
+ 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);
31
+ }
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();
53
+ }
54
+ 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;
71
+ }
72
+ if (target.closest && target.closest('.split-view-arrange-trigger')) return;
73
+ close(false);
74
+ }
75
+ export function initSplitViewCreateMenu() {
76
+ 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); });
83
+ }
@@ -0,0 +1,23 @@
1
+ // Pure request preparation for explicitly arranging Driver sessions.
2
+ import { preset, place, listIds } from './split-view-layout-client.js';
3
+
4
+ export function buildSplitViewCreateRequest(origins, sessions, activeSessionId, activeView) {
5
+ if (!Array.isArray(origins) || origins.length < 2 || origins.length > 8 || new Set(origins).size !== origins.length) return { ok: false, error: "Choose two to eight unique sessions." };
6
+ var selected = origins.map(function (origin) { return (sessions || []).find(function (session) { return session.sessionOriginId === origin; }); });
7
+ if (selected.some(function (session) { return !session || session.sessionRole === 'worker' || session.isMate || session.isTui; })) return { ok: false, error: "One or more selected sessions are no longer available." };
8
+ if (activeView) {
9
+ var currentOrigins = activeView.sessions.map(function (session) { return session.sessionOriginId; });
10
+ if (!currentOrigins.every(function (origin) { return origins.indexOf(origin) !== -1; })) return { ok: false, error: "Choose sessions from this Split view or from individual sessions." };
11
+ var tree = activeView.tree, sourceOrigin = activeView.activeSessionOriginId;
12
+ for (var i = 0; i < origins.length; i++) if (currentOrigins.indexOf(origins[i]) === -1) {
13
+ var placed = place(tree, origins[i], sourceOrigin, 'right', 0.5);
14
+ if (!placed.ok) return { ok: false, error: placed.reason || "Session could not be added." };
15
+ tree = placed.tree;
16
+ }
17
+ return { ok: true, type: 'project_split_view_update', body: { id: activeView.id, sessionOrigins: listIds(tree), tree: tree,
18
+ layout: activeView.layout, activeSessionOriginId: activeView.activeSessionOriginId } };
19
+ }
20
+ var tree = preset(origins, 'balanced'), current = selected.find(function (session) { return session.id === Number(activeSessionId); });
21
+ return { ok: true, type: 'project_split_view_create', body: { name: 'Split view', sessionOrigins: origins, tree: tree,
22
+ layout: 'balanced', activeSessionOriginId: current ? current.sessionOriginId : origins[0] }, meta: { activateCreatedView: true } };
23
+ }
@@ -0,0 +1,198 @@
1
+ // Browser-side parity for lib/split-view-layout.js. This module stays pure so
2
+ // client geometry and server validation can be exercised independently.
3
+ var MAX_SESSIONS = 8;
4
+ var MIN_RATIO = 0.05;
5
+ var MAX_RATIO = 0.95;
6
+ var PRESETS = ["balanced", "columns", "rows", "grid", "main-left", "main-right"];
7
+
8
+ export function listIds(tree) {
9
+ if (!tree || typeof tree !== "object") return [];
10
+ if (Object.prototype.hasOwnProperty.call(tree, "id")) return [tree.id];
11
+ return listIds(tree.left).concat(listIds(tree.right));
12
+ }
13
+
14
+ export function validateTree(tree, expectedIds) {
15
+ var ids = listIds(tree);
16
+ var valid = true;
17
+ function walk(node, depth) {
18
+ if (!node || typeof node !== "object" || depth > MAX_SESSIONS) return false;
19
+ if (Object.prototype.hasOwnProperty.call(node, "id")) {
20
+ return Object.keys(node).length === 1 && validId(node.id);
21
+ }
22
+ if (Object.keys(node).sort().join(",") !== "axis,left,ratio,right" ||
23
+ ["horizontal", "vertical"].indexOf(node.axis) === -1 || typeof node.ratio !== "number" ||
24
+ !Number.isFinite(node.ratio) || node.ratio < MIN_RATIO || node.ratio > MAX_RATIO) return false;
25
+ return walk(node.left, depth + 1) && walk(node.right, depth + 1);
26
+ }
27
+ valid = walk(tree, 0) && ids.length >= 2 && ids.length <= MAX_SESSIONS && new Set(ids).size === ids.length;
28
+ if (valid && expectedIds) {
29
+ valid = Array.isArray(expectedIds) && expectedIds.length === ids.length &&
30
+ expectedIds.every(function (id) { return ids.indexOf(id) !== -1; });
31
+ }
32
+ return { ok: valid, ids: ids };
33
+ }
34
+
35
+ function validId(value) {
36
+ return typeof value === "string" && value.length > 0 && value.length <= 128 && /^[A-Za-z0-9_-]+$/.test(value);
37
+ }
38
+ function leaf(id) { return { id: id }; }
39
+ export function pairTree(targetId, sourceId, position) {
40
+ if (!validId(targetId) || !validId(sourceId) || ["left", "right", "top", "bottom"].indexOf(position) === -1 || targetId === sourceId) {
41
+ throw new TypeError("Pair layout requires two distinct session origins and a valid position");
42
+ }
43
+ var before = position === "left" || position === "top";
44
+ return { axis: position === "top" || position === "bottom" ? "vertical" : "horizontal", ratio: 0.5,
45
+ left: before ? leaf(sourceId) : leaf(targetId), right: before ? leaf(targetId) : leaf(sourceId) };
46
+ }
47
+ function chain(ids, axis) {
48
+ if (ids.length === 1) return leaf(ids[0]);
49
+ return { axis: axis, ratio: 1 / ids.length, left: leaf(ids[0]), right: chain(ids.slice(1), axis) };
50
+ }
51
+ function grid(ids) {
52
+ if (ids.length <= 2) return chain(ids, "horizontal");
53
+ var split = Math.ceil(ids.length / 2);
54
+ return { axis: "vertical", ratio: split / ids.length,
55
+ left: chain(ids.slice(0, split), "horizontal"), right: chain(ids.slice(split), "horizontal") };
56
+ }
57
+
58
+ export function preset(ids, name) {
59
+ if (!Array.isArray(ids) || ids.length > MAX_SESSIONS || ids.some(function (id) { return !validId(id); }) || new Set(ids).size !== ids.length) {
60
+ throw new TypeError("Preset input must contain zero to eight unique session origins");
61
+ }
62
+ if (PRESETS.indexOf(name) === -1) throw new TypeError("Unknown Split view preset");
63
+ if (!ids.length) return null;
64
+ if (ids.length === 1) return leaf(ids[0]);
65
+ if (name === "columns") return chain(ids, "horizontal");
66
+ if (name === "rows") return chain(ids, "vertical");
67
+ if (name === "grid") return grid(ids);
68
+ if (name === "main-left" || name === "main-right") {
69
+ var share = ids.length === 2 ? 0.5 : 2 / 3;
70
+ return name === "main-left" ? { axis: "horizontal", ratio: share, left: leaf(ids[0]), right: grid(ids.slice(1)) } :
71
+ { axis: "horizontal", ratio: 1 - share, left: grid(ids.slice(1)), right: leaf(ids[0]) };
72
+ }
73
+ if (ids.length <= 3) return { axis: "horizontal", ratio: 0.55, left: leaf(ids[0]), right: chain(ids.slice(1), "vertical") };
74
+ return grid(ids);
75
+ }
76
+
77
+ export function remove(tree, id) {
78
+ if (!tree || typeof tree !== "object") return tree;
79
+ if (Object.prototype.hasOwnProperty.call(tree, "id")) return tree.id === id ? null : tree;
80
+ var left = remove(tree.left, id), right = remove(tree.right, id);
81
+ if (!left) return right;
82
+ if (!right) return left;
83
+ if (left === tree.left && right === tree.right) return tree;
84
+ return { axis: tree.axis, ratio: tree.ratio, left: left, right: right };
85
+ }
86
+
87
+ function insert(tree, id, targetId, position, share) {
88
+ if (!tree || typeof tree !== "object") return { tree: tree, found: false };
89
+ if (Object.prototype.hasOwnProperty.call(tree, "id")) {
90
+ if (tree.id !== targetId) return { tree: tree, found: false };
91
+ var horizontal = position === "left" || position === "right";
92
+ var before = position === "left" || position === "top";
93
+ var branch = { axis: horizontal ? "horizontal" : "vertical", ratio: before ? share : 1 - share,
94
+ left: before ? leaf(id) : tree, right: before ? tree : leaf(id) };
95
+ return { tree: branch, found: true };
96
+ }
97
+ var left = insert(tree.left, id, targetId, position, share);
98
+ if (left.found) return { tree: { axis: tree.axis, ratio: tree.ratio, left: left.tree, right: tree.right }, found: true };
99
+ var right = insert(tree.right, id, targetId, position, share);
100
+ if (right.found) return { tree: { axis: tree.axis, ratio: tree.ratio, left: tree.left, right: right.tree }, found: true };
101
+ return { tree: tree, found: false };
102
+ }
103
+
104
+ export function place(tree, sourceId, targetId, position, share) {
105
+ var allowed = ["left", "right", "top", "bottom"];
106
+ var amount = share === undefined ? 0.5 : share;
107
+ if (!validId(sourceId) || !validId(targetId)) return { ok: false, tree: tree, reason: "invalid-id" };
108
+ if (allowed.indexOf(position) === -1) return { ok: false, tree: tree, reason: "invalid-position" };
109
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount < MIN_RATIO || amount > MAX_RATIO) return { ok: false, tree: tree, reason: "invalid-share" };
110
+ if (sourceId === targetId) return { ok: true, tree: tree, changed: false };
111
+ if (!tree) return { ok: false, tree: tree, reason: "unknown-target" };
112
+ var targetIds = listIds(tree);
113
+ if (targetIds.indexOf(targetId) === -1) return { ok: false, tree: tree, reason: "unknown-target" };
114
+ var sourceExists = targetIds.indexOf(sourceId) !== -1;
115
+ if (!sourceExists && targetIds.length >= MAX_SESSIONS) return { ok: false, tree: tree, reason: "limit" };
116
+ var base = sourceExists ? remove(tree, sourceId) : tree;
117
+ var result = insert(base, sourceId, targetId, position, amount);
118
+ if (!result.found) return { ok: false, tree: tree, reason: "unknown-target" };
119
+ return { ok: true, tree: result.tree, changed: true };
120
+ }
121
+
122
+ export function swap(tree, firstId, secondId) {
123
+ var ids = listIds(tree);
124
+ if (firstId === secondId) return { ok: ids.indexOf(firstId) !== -1, tree: tree, changed: false, reason: ids.indexOf(firstId) === -1 ? "unknown-id" : null };
125
+ if (ids.indexOf(firstId) === -1 || ids.indexOf(secondId) === -1) return { ok: false, tree: tree, changed: false, reason: "unknown-id" };
126
+ function walk(node) {
127
+ if (Object.prototype.hasOwnProperty.call(node, "id")) return leaf(node.id === firstId ? secondId : node.id === secondId ? firstId : node.id);
128
+ return { axis: node.axis, ratio: node.ratio, left: walk(node.left), right: walk(node.right) };
129
+ }
130
+ return { ok: true, tree: walk(tree), changed: true };
131
+ }
132
+
133
+ export function resize(tree, path, ratio) {
134
+ if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio < MIN_RATIO || ratio > MAX_RATIO) return null;
135
+ if (!Array.isArray(path)) return null;
136
+ function walk(node, index) {
137
+ if (!node || Object.prototype.hasOwnProperty.call(node, "id")) return null;
138
+ if (index === path.length) return { axis: node.axis, ratio: ratio, left: node.left, right: node.right };
139
+ var side = path[index];
140
+ if (side !== "left" && side !== "right") return null;
141
+ var child = walk(node[side], index + 1);
142
+ if (!child) return null;
143
+ return { axis: node.axis, ratio: node.ratio, left: side === "left" ? child : node.left, right: side === "right" ? child : node.right };
144
+ }
145
+ return walk(tree, 0);
146
+ }
147
+
148
+ export function arrangeRegion(tree, sourceId, position) {
149
+ var positions = ["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right", "left-third", "right-third"];
150
+ if (positions.indexOf(position) === -1) return { ok: false, tree: tree, reason: "invalid-position" };
151
+ var ids = listIds(tree), remaining = ids.filter(function (id) { return id !== sourceId; });
152
+ if (!validId(sourceId) || ids.indexOf(sourceId) === -1) return { ok: false, tree: tree, reason: "unknown-source" };
153
+ if (!remaining.length) return { ok: false, tree: tree, reason: "insufficient-windows" };
154
+ var rest = preset(remaining, "balanced");
155
+ if (position === "left-third" || position === "right-third") {
156
+ var leftThird = position === "left-third";
157
+ return { ok: true, tree: leftThird ? { axis: "horizontal", ratio: 1 / 3, left: leaf(sourceId), right: rest } :
158
+ { axis: "horizontal", ratio: 2 / 3, left: rest, right: leaf(sourceId) } };
159
+ }
160
+ if (position.indexOf("-") !== -1) {
161
+ if (remaining.length < 2) return { ok: false, tree: tree, reason: "quarter-requires-three" };
162
+ var top = position.indexOf("top-") === 0, left = position.slice(-4) === "left", companion = remaining[0];
163
+ var bulk = preset(remaining.slice(1), "balanced");
164
+ var corner = top ? { axis: "vertical", ratio: 0.5, left: leaf(sourceId), right: leaf(companion) } :
165
+ { axis: "vertical", ratio: 0.5, left: leaf(companion), right: leaf(sourceId) };
166
+ return { ok: true, tree: left ? { axis: "horizontal", ratio: 0.5, left: corner, right: bulk } :
167
+ { axis: "horizontal", ratio: 0.5, left: bulk, right: corner } };
168
+ }
169
+ var horizontal = position === "left" || position === "right", before = position === "left" || position === "top";
170
+ return { ok: true, tree: { axis: horizontal ? "horizontal" : "vertical", ratio: 0.5,
171
+ left: before ? leaf(sourceId) : rest, right: before ? rest : leaf(sourceId) } };
172
+ }
173
+
174
+ // Rectangles are returned by leaf ID and divider path. The host owns flat pane
175
+ // nodes; callers apply these numbers as inline positions without reparenting.
176
+ export function rectangles(tree, width, height, gap) {
177
+ var paneRects = {};
178
+ var dividers = [];
179
+ var dividerSize = Number.isFinite(gap) && gap >= 0 ? gap : 7;
180
+ function visit(node, rect, path) {
181
+ if (!node) return;
182
+ if (Object.prototype.hasOwnProperty.call(node, "id")) { paneRects[node.id] = rect; return; }
183
+ var horizontal = node.axis === "horizontal";
184
+ var available = Math.max(0, (horizontal ? rect.width : rect.height) - dividerSize);
185
+ var first = available * node.ratio;
186
+ var split = horizontal ? rect.x + first : rect.y + first;
187
+ var divider = horizontal ? { x: split, y: rect.y, width: dividerSize, height: rect.height } :
188
+ { x: rect.x, y: split, width: rect.width, height: dividerSize };
189
+ dividers.push({ path: path.slice(), axis: node.axis, rect: divider, branchRect: rect, ratio: node.ratio, available: available });
190
+ var left = horizontal ? { x: rect.x, y: rect.y, width: first, height: rect.height } : { x: rect.x, y: rect.y, width: rect.width, height: first };
191
+ var right = horizontal ? { x: split + dividerSize, y: rect.y, width: available - first, height: rect.height } :
192
+ { x: rect.x, y: split + dividerSize, width: rect.width, height: available - first };
193
+ visit(node.left, left, path.concat("left"));
194
+ visit(node.right, right, path.concat("right"));
195
+ }
196
+ visit(tree, { x: 0, y: 0, width: width, height: height }, []);
197
+ return { panes: paneRects, dividers: dividers };
198
+ }
@@ -0,0 +1,37 @@
1
+ // Inline, accessible layout previews for the presentation Arrange chooser.
2
+ var PRESET_TILES = {
3
+ balanced: [[3, 3, 16, 18], [21, 3, 12, 8], [21, 13, 12, 8]],
4
+ columns: [[3, 3, 8, 18], [13, 3, 8, 18], [23, 3, 10, 18]],
5
+ rows: [[3, 3, 30, 4], [3, 9, 30, 5], [3, 16, 30, 5]],
6
+ grid: [[3, 3, 14, 8], [19, 3, 14, 8], [3, 13, 14, 8], [19, 13, 14, 8]],
7
+ "main-left": [[3, 3, 19, 18], [24, 3, 9, 8], [24, 13, 9, 8]],
8
+ "main-right": [[14, 3, 19, 18], [3, 3, 9, 8], [3, 13, 9, 8]]
9
+ };
10
+ var POSITION_TILES = {
11
+ left: [[3, 3, 14, 18], [19, 3, 14, 18]], right: [[19, 3, 14, 18], [3, 3, 14, 18]],
12
+ top: [[3, 3, 30, 8], [3, 13, 30, 8]], bottom: [[3, 13, 30, 8], [3, 3, 30, 8]],
13
+ "top-left": [[3, 3, 14, 8], [19, 3, 14, 8], [3, 13, 14, 8], [19, 13, 14, 8]],
14
+ "top-right": [[19, 3, 14, 8], [3, 3, 14, 8], [19, 13, 14, 8], [3, 13, 14, 8]],
15
+ "bottom-left": [[3, 13, 14, 8], [19, 13, 14, 8], [3, 3, 14, 8], [19, 3, 14, 8]],
16
+ "bottom-right": [[19, 13, 14, 8], [3, 13, 14, 8], [19, 3, 14, 8], [3, 3, 14, 8]],
17
+ "left-third": [[3, 3, 8, 18], [13, 3, 20, 18]], "right-third": [[23, 3, 10, 18], [3, 3, 18, 18]]
18
+ };
19
+
20
+ export function appendLayoutOption(button, label, kind, value) {
21
+ var tiles = kind === "preset" ? PRESET_TILES[value] : POSITION_TILES[value];
22
+ if (!tiles) return false;
23
+ var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
24
+ svg.setAttribute("class", "split-view-layout-icon"); svg.setAttribute("viewBox", "0 0 36 24");
25
+ svg.setAttribute("aria-hidden", "true" );
26
+ var frame = document.createElementNS("http://www.w3.org/2000/svg", "rect");
27
+ frame.setAttribute("class", "split-view-layout-frame"); frame.setAttribute("x", "1"); frame.setAttribute("y", "1");
28
+ frame.setAttribute("width", "34"); frame.setAttribute("height", "22"); frame.setAttribute("rx", "2"); svg.appendChild(frame);
29
+ tiles.forEach(function (tile, index) {
30
+ var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
31
+ rect.setAttribute("class", "split-view-layout-tile" + (index === 0 ? " split-view-layout-selected" : ""));
32
+ rect.setAttribute("x", String(tile[0])); rect.setAttribute("y", String(tile[1]));
33
+ rect.setAttribute("width", String(tile[2])); rect.setAttribute("height", String(tile[3])); rect.setAttribute("rx", "1"); svg.appendChild(rect);
34
+ });
35
+ var text = document.createElement("span"); text.className = "split-view-layout-label"; text.textContent = label;
36
+ button.appendChild(svg); button.appendChild(text); return true;
37
+ }
@@ -0,0 +1,38 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+
4
+ // Only serializable receipts enter store. The transport itself stays module-owned.
5
+ var layoutSocket = null;
6
+ var layoutSocketEpoch = 0;
7
+ var layoutContextEpoch = 0;
8
+ store.subscribe(function (state, previous) {
9
+ var fields = ['projectSplitViewActiveId', 'projectSplitViewContextKey', 'currentSlug', 'currentProjectOwnerId', 'myUserId', 'projectSplitViewSocketGeneration', 'connected'];
10
+ function activeVersion(value) {
11
+ var view = (value.projectSplitViews || []).find(function (item) { return item.id === value.projectSplitViewActiveId; });
12
+ return view ? JSON.stringify([view.revision, view.tree]) : '';
13
+ }
14
+ if (fields.some(function (key) { return state[key] !== previous[key]; }) || activeVersion(state) !== activeVersion(previous)) layoutContextEpoch++;
15
+ });
16
+ function socketEpoch() {
17
+ var socket = getWs();
18
+ if (socket !== layoutSocket) { layoutSocket = socket; layoutSocketEpoch++; }
19
+ return layoutSocketEpoch;
20
+ }
21
+ export function layoutScope(view) {
22
+ var state = store.snap();
23
+ return view && { epoch: layoutContextEpoch, viewId: view.id, revision: view.revision, tree: JSON.stringify(view.tree),
24
+ context: state.projectSplitViewContextKey, slug: state.currentSlug, owner: state.currentProjectOwnerId,
25
+ viewer: state.myUserId, generation: state.projectSplitViewSocketGeneration, socketEpoch: socketEpoch() };
26
+ }
27
+ export function currentLayoutScope(receipt) {
28
+ var state = store.snap();
29
+ if (!receipt || state.connected === false || state.projectSplitViewActiveId !== receipt.viewId) return false;
30
+ var view = (state.projectSplitViews || []).find(function (item) { return item.id === receipt.viewId; });
31
+ return !!view && JSON.stringify(layoutScope(view)) === JSON.stringify(receipt);
32
+ }
33
+ export function presentationTree(view) {
34
+ var receipt = store.get('projectSplitViewPreviewScope');
35
+ return view && receipt && receipt.viewId === view.id && store.get('projectSplitViewPreviewTree') && currentLayoutScope(receipt) &&
36
+ JSON.stringify(receipt) === JSON.stringify(layoutScope(view)) ?
37
+ store.get('projectSplitViewPreviewTree') : view && view.tree;
38
+ }