clay-server 5.3.0-beta.4 → 5.3.0-beta.6
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.
- package/lib/ask-user-request-identity.js +18 -1
- package/lib/clay-product-control-tools.js +2 -2
- package/lib/clay-product-control.js +1 -1
- package/lib/clay-screen-controls.js +1 -0
- package/lib/clay-split-view-awareness.js +5 -2
- package/lib/clay-split-view-controls.js +14 -7
- package/lib/clay-split-view-input.js +5 -4
- package/lib/clay-split-view-render-proof.js +14 -4
- package/lib/clay-view-workspace-input.js +71 -0
- package/lib/project-delegated-follow-up.js +18 -9
- package/lib/project-file-history.js +11 -9
- package/lib/project-file-watch.js +26 -7
- package/lib/project-filesystem.js +5 -4
- package/lib/project-shared-browser.js +7 -3
- package/lib/project-split-view-document.js +16 -2
- package/lib/project-split-views.js +15 -294
- package/lib/project-user-message.js +4 -3
- package/lib/project-vendor-login.js +2 -2
- package/lib/project-view-input.js +11 -0
- package/lib/project-view-pair.js +26 -0
- package/lib/project-view-store.js +346 -0
- package/lib/project-view-terminal-binding.js +16 -0
- package/lib/project-view-workspace.js +126 -0
- package/lib/project.js +1 -0
- package/lib/public/app.js +2 -0
- package/lib/public/css/issues.css +3 -0
- package/lib/public/css/pane.css +5 -1
- package/lib/public/css/right-workbench.css +25 -3
- package/lib/public/css/scheduled-tasks.css +7 -0
- package/lib/public/css/shared-browser.css +21 -0
- package/lib/public/css/split-view-layout.css +37 -3
- package/lib/public/css/sticky-notes.css +3 -2
- package/lib/public/css/tool-pane.css +127 -0
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-header.js +24 -2
- package/lib/public/modules/app-messages.js +1 -1
- package/lib/public/modules/clay-file-links.js +2 -1
- package/lib/public/modules/clay-session-links.js +1 -1
- package/lib/public/modules/clay-split-view-screen.js +6 -3
- package/lib/public/modules/clay-tool-pane-screen.js +35 -0
- package/lib/public/modules/file-history-snapshot.js +12 -0
- package/lib/public/modules/file-pane-entrypoints.js +55 -0
- package/lib/public/modules/file-pane-history.js +78 -0
- package/lib/public/modules/file-pane-instances.js +140 -0
- package/lib/public/modules/file-pane-links.js +41 -0
- package/lib/public/modules/file-pane-render.js +48 -0
- package/lib/public/modules/file-pane-review.js +26 -0
- package/lib/public/modules/file-pane-toolbar.js +54 -0
- package/lib/public/modules/file-viewer-source.js +70 -0
- package/lib/public/modules/filebrowser.js +44 -83
- package/lib/public/modules/git-panel.js +5 -0
- package/lib/public/modules/issues.js +3 -4
- package/lib/public/modules/linear-panel.js +2 -2
- package/lib/public/modules/mate-knowledge-workbench.js +10 -6
- package/lib/public/modules/notifications.js +4 -1
- package/lib/public/modules/pane-activation.js +5 -0
- package/lib/public/modules/pane-binding.js +5 -1
- package/lib/public/modules/pane-browser-bridge.js +20 -6
- package/lib/public/modules/pane-browser-transport.js +110 -0
- package/lib/public/modules/pane-header-runtime.js +18 -0
- package/lib/public/modules/plantuml-preview.js +12 -12
- package/lib/public/modules/right-workbench.js +116 -67
- package/lib/public/modules/scheduled-tasks.js +7 -6
- package/lib/public/modules/shared-browser.js +75 -9
- package/lib/public/modules/split-pane-renderer.js +7 -8
- package/lib/public/modules/split-pane-runtime.js +23 -0
- package/lib/public/modules/split-view-drag-preview.js +90 -0
- package/lib/public/modules/split-view-layout-client.js +1 -1
- package/lib/public/modules/split-view-presentation-drag.js +36 -13
- package/lib/public/modules/split-view-presentation-drop.js +21 -18
- package/lib/public/modules/split-view-presentation-renderer.js +15 -10
- package/lib/public/modules/split-view-presentations.js +25 -33
- package/lib/public/modules/split-view.js +17 -16
- package/lib/public/modules/sticky-notes-browser.js +2 -2
- package/lib/public/modules/sticky-notes.js +8 -10
- package/lib/public/modules/terminal.js +45 -13
- package/lib/public/modules/theme.js +1 -1
- package/lib/public/modules/tool-pane-chrome.js +82 -0
- package/lib/public/modules/tool-pane-drag.js +134 -0
- package/lib/public/modules/tool-pane-layout.js +95 -0
- package/lib/public/modules/tool-pane-legacy.js +60 -0
- package/lib/public/modules/tool-pane-tree.js +51 -0
- package/lib/public/modules/view-legacy-coordinates.js +16 -0
- package/lib/public/modules/view-mutations.js +24 -0
- package/lib/public/modules/view-pair-presentation.js +74 -0
- package/lib/public/modules/view-pair-validation.js +12 -0
- package/lib/public/modules/view-presentation-state.js +37 -0
- package/lib/public/modules/view-workspace-controller.js +210 -0
- package/lib/public/modules/view-workspace-state.js +103 -0
- package/lib/public/modules/view-workspace-validation.js +47 -0
- package/lib/public/modules/wireframe-workbench.js +11 -0
- package/lib/public/modules/workbench-layout-preference.js +12 -5
- package/lib/public/modules/workbench-panels.js +21 -0
- package/lib/public/modules/workspace-assignment-card.js +9 -6
- package/lib/public/pane-assets/{app-ED2VCGNQ.js → app-7UZ727NA.js} +1 -1
- package/lib/public/pane-assets/chunk-2AIWCI6Y.js +1 -0
- package/lib/public/pane-assets/chunk-6MIHIQ2W.js +1 -0
- package/lib/public/pane-assets/chunk-6NYCNWVF.js +1 -0
- package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
- package/lib/public/pane-assets/chunk-6UJ2ALOB.js +1 -0
- package/lib/public/pane-assets/chunk-6XK2OATZ.js +1 -0
- package/lib/public/pane-assets/chunk-BKIDR2ME.js +1 -0
- package/lib/public/pane-assets/chunk-BMHTBRMO.js +1 -0
- package/lib/public/pane-assets/chunk-GEFCFZEN.js +1 -0
- package/lib/public/pane-assets/chunk-GN2NQEVC.js +2 -0
- package/lib/public/pane-assets/chunk-IMLMBPT4.js +166 -0
- package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
- package/lib/public/pane-assets/chunk-K5S6AILM.js +1 -0
- package/lib/public/pane-assets/chunk-LDIZLKB2.js +1 -0
- package/lib/public/pane-assets/chunk-OWNNLZ36.js +1 -0
- package/lib/public/pane-assets/chunk-P2C3LOWL.js +3 -0
- package/lib/public/pane-assets/chunk-PV4PCH2V.js +2 -0
- package/lib/public/pane-assets/chunk-R4LMPTKU.js +1 -0
- package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
- package/lib/public/pane-assets/chunk-TMGSDFJI.js +1 -0
- package/lib/public/pane-assets/chunk-VKYBAVWT.js +1 -0
- package/lib/public/pane-assets/chunk-VNAV23CM.js +1 -0
- package/lib/public/pane-assets/chunk-VWTHIQCE.js +1 -0
- package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
- package/lib/public/pane-assets/chunk-WM3K5AFH.js +1 -0
- package/lib/public/pane-assets/chunk-XOB3O327.js +1 -0
- package/lib/public/pane-assets/chunk-XOOUCFYT.js +1 -0
- package/lib/public/pane-assets/manifest.json +383 -336
- package/lib/public/pane-assets/style-ZM7OOU3M.css +1 -0
- package/lib/public/style.css +1 -0
- package/lib/server-workbench-layout-settings.js +3 -3
- package/lib/server.js +1 -0
- package/lib/split-view-layout.js +11 -7
- package/lib/users-workbench-layout-preferences.js +14 -4
- package/lib/workbench-window-layout.js +30 -0
- package/lib/workspace-assignment-service.js +56 -11
- package/lib/workspace-follow-up-queue.js +92 -0
- package/lib/workspace-query-mcp-server.js +1 -1
- package/package.json +1 -1
- package/lib/public/pane-assets/chunk-6D4DPE5X.js +0 -1
- package/lib/public/pane-assets/chunk-6SM2WQJX.js +0 -1
- package/lib/public/pane-assets/chunk-7H36SUZA.js +0 -2
- package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
- package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
- package/lib/public/pane-assets/chunk-IYFILQHZ.js +0 -2
- package/lib/public/pane-assets/chunk-IZ2ZANEY.js +0 -167
- package/lib/public/pane-assets/chunk-JAEQCN5S.js +0 -1
- package/lib/public/pane-assets/chunk-ST2Q62ET.js +0 -1
- package/lib/public/pane-assets/style-JVCOCG3W.css +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ensureArrangeTrigger } from './split-view-presentation-renderer.js';
|
|
2
|
+
import { pairBase } from './view-pair-presentation.js';
|
|
3
|
+
import { activeSavedView } from './view-workspace-state.js';
|
|
4
|
+
// Legacy Driver/Worker pairs use the same display-only window geometry.
|
|
5
|
+
import { store } from './store.js';
|
|
6
|
+
import { rectangles } from './split-view-layout-client.js';
|
|
7
|
+
import { toolPaneLayoutTree, paintToolPaneWindows, bindWorkspaceDivider, dockedToolPanes } from './tool-pane-layout.js';
|
|
8
|
+
|
|
9
|
+
export function clearToolPaneWindows() {
|
|
10
|
+
document.querySelectorAll('.tool-pane-window').forEach(function (panel) {
|
|
11
|
+
panel.classList.remove('tool-pane-window'); delete panel.dataset.toolWindowId;
|
|
12
|
+
['left', 'top', 'width', 'height', 'right', 'maxHeight'].forEach(function (field) { panel.style[field] = ''; });
|
|
13
|
+
});
|
|
14
|
+
var main = document.getElementById('main-panels');
|
|
15
|
+
if (main) main.classList.remove('has-tool-pane-windows');
|
|
16
|
+
store.set({ toolPaneWindowGeometry: null });
|
|
17
|
+
}
|
|
18
|
+
export function paintLegacyToolPanes(host) {
|
|
19
|
+
var split = store.get('splitPanes');
|
|
20
|
+
if (!host || !split || !split.panes || split.panes.length < 2) return false;
|
|
21
|
+
var panes = Array.from(host.querySelectorAll(':scope > .split-pane:not(.split-pane-cached)'));
|
|
22
|
+
var ids = panes.map(function (pane) {
|
|
23
|
+
var frame = pane.querySelector('.split-pane-frame');
|
|
24
|
+
if (frame && !frame.dataset.toolPaneGeometryReadyBound) {
|
|
25
|
+
frame.dataset.toolPaneGeometryReadyBound = 'true';
|
|
26
|
+
frame.addEventListener('clay-pane-ready', function () { queueMicrotask(function () { if (frame.isConnected) paintLegacyToolPanes(host); }); });
|
|
27
|
+
}
|
|
28
|
+
return frame && frame.dataset.sessionOriginId;
|
|
29
|
+
});
|
|
30
|
+
if (ids.length < split.panes.length || ids.some(function (id) { return !id; }) || new Set(ids).size !== ids.length) return false;
|
|
31
|
+
var base = { id: ids[0] };
|
|
32
|
+
ids.slice(1).forEach(function (id) { base = { axis: 'horizontal', ratio: .5, left: base, right: { id: id } }; });
|
|
33
|
+
var view = activeSavedView() || { id: 'legacy-' + (split.groupId || ids.join('-')), activeSessionOriginId: ids[0] };
|
|
34
|
+
if (view.workspace && view.workspace.linkedPair) base = pairBase(view.workspace.linkedPair, view.tree);
|
|
35
|
+
host.classList.add('split-view-canvas'); host.dataset.positioned = 'true'; delete host.dataset.singlePane;
|
|
36
|
+
var style = getComputedStyle(host), left = parseFloat(style.paddingLeft) || 0, top = parseFloat(style.paddingTop) || 0;
|
|
37
|
+
var width = host.clientWidth - left - (parseFloat(style.paddingRight) || 0), height = host.clientHeight - top - (parseFloat(style.paddingBottom) || 0);
|
|
38
|
+
var tree = toolPaneLayoutTree(view, base), geometry = rectangles(tree, Math.max(0, width), Math.max(0, height), 7);
|
|
39
|
+
paintToolPaneWindows(host, view, base, tree, geometry, { left: left, top: top });
|
|
40
|
+
var focused = store.get('projectSplitViewFocusedOrigin');
|
|
41
|
+
panes.forEach(function (pane, index) {
|
|
42
|
+
var rect = geometry.panes[ids[index]];
|
|
43
|
+
pane.hidden = !!focused && ids[index] !== focused; pane.style.display = pane.hidden ? 'none' : '';
|
|
44
|
+
if (focused === ids[index]) rect = { x: 0, y: 0, width: width, height: height };
|
|
45
|
+
if (!rect) return;
|
|
46
|
+
if (view.sessions && view.sessions.some(function (s) { return s.sessionOriginId === ids[index]; })) ensureArrangeTrigger(pane.querySelector('.split-pane-header'));
|
|
47
|
+
pane.style.left = left + rect.x + 'px'; pane.style.top = top + rect.y + 'px';
|
|
48
|
+
pane.style.width = rect.width + 'px'; pane.style.height = rect.height + 'px';
|
|
49
|
+
});
|
|
50
|
+
host.querySelectorAll(':scope > .split-view-divider').forEach(function (divider) { divider.remove(); });
|
|
51
|
+
(focused ? [] : geometry.dividers).forEach(function (entry) {
|
|
52
|
+
var divider = document.createElement('div'); divider.className = 'split-view-divider'; divider.dataset.axis = entry.axis;
|
|
53
|
+
divider.setAttribute('role', 'separator'); divider.setAttribute('tabindex', '0'); divider.setAttribute('aria-label', 'Resize pane windows');
|
|
54
|
+
divider.setAttribute('aria-orientation', entry.axis === 'horizontal' ? 'vertical' : 'horizontal');
|
|
55
|
+
divider.style.left = left + entry.rect.x + 'px'; divider.style.top = top + entry.rect.y + 'px';
|
|
56
|
+
divider.style.width = entry.rect.width + 'px'; divider.style.height = entry.rect.height + 'px';
|
|
57
|
+
bindWorkspaceDivider(divider, entry, tree); host.appendChild(divider);
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { WORKBENCH_PANELS } from './workbench-panels.js';
|
|
2
|
+
import { listIds, remove, pairTree, swap } from './split-view-layout-client.js';
|
|
3
|
+
|
|
4
|
+
export var TOOL_PANES = { terminal: { id: 'terminal-container', label: 'Terminal', icon: 'terminal' },
|
|
5
|
+
browser: { id: 'shared-browser-panel', label: 'Browser', icon: 'globe' }, 'file-viewer': { id: 'file-viewer', label: 'Files', icon: 'file-text' } };
|
|
6
|
+
Object.assign(TOOL_PANES, WORKBENCH_PANELS);
|
|
7
|
+
export function toolWindowId(name) { return 'tool-' + name; }
|
|
8
|
+
export function isToolWindow(id) { return /^tool-file-[A-Za-z0-9_-]+$/.test(id) || Object.keys(TOOL_PANES).some(function (name) { return toolWindowId(name) === id; }); }
|
|
9
|
+
export function insertWindow(tree, source, target, side, ratio) {
|
|
10
|
+
if (source === target) return tree;
|
|
11
|
+
if (side === 'center' && isToolWindow(source) && isToolWindow(target)) return swap(tree, source, target).tree;
|
|
12
|
+
side = side === 'center' ? 'right' : side;
|
|
13
|
+
var base = remove(tree, source), found = false;
|
|
14
|
+
function walk(node) {
|
|
15
|
+
if (!node) return node;
|
|
16
|
+
if (node.id) {
|
|
17
|
+
if (node.id !== target) return node;
|
|
18
|
+
found = true;
|
|
19
|
+
var result = pairTree(target, source, side);
|
|
20
|
+
result.ratio = ratio || .5;
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
return { axis: node.axis, ratio: node.ratio, left: walk(node.left), right: walk(node.right) };
|
|
24
|
+
}
|
|
25
|
+
var next = walk(base);
|
|
26
|
+
return found && listIds(next).length <= 24 ? next : tree;
|
|
27
|
+
}
|
|
28
|
+
function stripUnknown(tree, allowed) {
|
|
29
|
+
var result = tree;
|
|
30
|
+
listIds(tree).forEach(function (id) { if (allowed.indexOf(id) < 0) result = remove(result, id); });
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function placements(tree, output) {
|
|
34
|
+
if (!tree || tree.id) return;
|
|
35
|
+
var left = listIds(tree.left), right = listIds(tree.right);
|
|
36
|
+
if (tree.left.id && isToolWindow(tree.left.id)) output.push({ id: tree.left.id, target: right[0], side: tree.axis === 'horizontal' ? 'left' : 'top', ratio: tree.ratio });
|
|
37
|
+
else if (tree.right.id && isToolWindow(tree.right.id)) output.push({ id: tree.right.id, target: left[0], side: tree.axis === 'horizontal' ? 'right' : 'bottom', ratio: tree.ratio });
|
|
38
|
+
placements(tree.left, output); placements(tree.right, output);
|
|
39
|
+
}
|
|
40
|
+
export function workspaceWindowTree(base, saved, owners) {
|
|
41
|
+
var sessions = listIds(base), tools = owners.map(toolWindowId), allowed = sessions.concat(tools), tree = base;
|
|
42
|
+
if (saved && saved.tree && JSON.stringify(saved.base) === JSON.stringify(base)) tree = stripUnknown(saved.tree, allowed);
|
|
43
|
+
else if (saved && saved.tree) {
|
|
44
|
+
var attached = []; placements(saved.tree, attached);
|
|
45
|
+
attached.reverse().forEach(function (item) { if (tools.indexOf(item.id) >= 0 && listIds(tree).indexOf(item.target) >= 0) tree = insertWindow(tree, item.id, item.target, item.side, item.ratio); });
|
|
46
|
+
}
|
|
47
|
+
tools.forEach(function (id) {
|
|
48
|
+
if (listIds(tree).indexOf(id) < 0) tree = tree ? { axis: 'horizontal', ratio: .66, left: tree, right: { id: id } } : { id: id };
|
|
49
|
+
});
|
|
50
|
+
return tree;
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { copy, mapWorkspaceTree, paneOwner } from './view-workspace-state.js';
|
|
2
|
+
import { listIds } from './split-view-layout-client.js';
|
|
3
|
+
// Coordinates never create resources. Resolve the old singleton Files slot only
|
|
4
|
+
// after a real file instance exists; leave other delayed slots intact.
|
|
5
|
+
export function boundLegacyCoordinates(record, value) {
|
|
6
|
+
var next = copy(record), mapping = {};
|
|
7
|
+
listIds(record.tree).forEach(function (id) { mapping[id] = id; });
|
|
8
|
+
var files = value.panes.filter(function (pane) { return pane.kind === 'file' && pane.open && pane.placement === 'docked'; });
|
|
9
|
+
if (files.length === 1) mapping['tool-file-viewer'] = 'tool-' + paneOwner(files[0]);
|
|
10
|
+
next.tree = mapWorkspaceTree(next.tree, mapping);
|
|
11
|
+
return next;
|
|
12
|
+
}
|
|
13
|
+
export function legacyCoordinatesReady(record, value) {
|
|
14
|
+
var ids = value.panes.filter(function (p) { return p.open && p.placement === 'docked'; }).map(function (p) { return 'tool-' + paneOwner(p); });
|
|
15
|
+
return listIds(boundLegacyCoordinates(record, value).tree).filter(function (id) { return id.indexOf('tool-') === 0; }).every(function (id) { return ids.indexOf(id) >= 0; });
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
import { prepareViewMutation } from './view-workspace-state.js';
|
|
5
|
+
function contextKey(state) { return [state.currentSlug || '', state.currentProjectOwnerId || '', state.myUserId || '', state.projectSplitViewSocketGeneration || 0].join('|'); }
|
|
6
|
+
function projectReady(state) { return getWs() !== state.splitIdentityBlockedSocket && !state.paneMode && !state.dmMode && state.connected && state.authIdentityLoaded && state.currentSlug === state.activeProjectSlug && state.currentSlug === state.sessionListProjectSlug; }
|
|
7
|
+
function sendMessage(message) { var ws = getWs(); if (!ws || ws.readyState !== 1 || !projectReady(store.snap())) return false; try { ws.send(JSON.stringify(message)); return true; } catch (error) { return false; } }
|
|
8
|
+
export function mutation(type, body, meta) {
|
|
9
|
+
var state = store.snap();
|
|
10
|
+
if (!projectReady(state) || !state.projectSplitViewHydrated || state.projectSplitViewError || state.projectSplitViewPending) return false;
|
|
11
|
+
var requestId = "split-view-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
|
|
12
|
+
var ws = getWs();
|
|
13
|
+
var message = prepareViewMutation(type, Object.assign({ type: type, requestId: requestId, expectedRevision: state.projectSplitViewRevision }, body || {}));
|
|
14
|
+
if (!message) return false;
|
|
15
|
+
var pending = { requestId: requestId, type: type, socket: ws, contextKey: contextKey(state), expectedRevision: state.projectSplitViewRevision, meta: meta || null,
|
|
16
|
+
awaitingBroadcast: false, resultRevision: null };
|
|
17
|
+
store.set({ projectSplitViewPending: pending, projectSplitViewError: "" });
|
|
18
|
+
if (!sendMessage(message)) {
|
|
19
|
+
store.set({ projectSplitViewPending: null, projectSplitViewError: "Split view change could not be sent. Refresh before trying again." });
|
|
20
|
+
showToast("Split view change could not be sent. Refresh before trying again.", "error");
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Linked Workers are a read-only presentation of the existing pair registry.
|
|
2
|
+
// They never enter the saved View's ordinary session membership.
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
import { store } from './store.js';
|
|
5
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
6
|
+
import { splitGroupRoles } from './split-group-helpers.js';
|
|
7
|
+
import { splitPaneForSession } from './split-pane-reconciler.js';
|
|
8
|
+
import { mutation } from './view-mutations.js';
|
|
9
|
+
import { activeSavedView, copy, displayTree, mapWorkspaceTree } from './view-workspace-state.js';
|
|
10
|
+
|
|
11
|
+
export function currentViewPair() {
|
|
12
|
+
var state = store.snap(), split = state.splitPanes;
|
|
13
|
+
if (!split || !state.projectSplitViewHydrated || state.paneMode || !state.connected || !state.authIdentityLoaded ||
|
|
14
|
+
state.currentSlug !== state.sessionListProjectSlug || state.currentSlug !== state.activeProjectSlug) return null;
|
|
15
|
+
var group = (state.splitGroups || []).find(function (item) { return item.id === split.groupId; }), roles = splitGroupRoles(group);
|
|
16
|
+
if (!roles || roles.orderedIds.length > 8 || split.panes.length !== roles.orderedIds.length || split.panes.some(function (pane) { return pane.slug !== state.currentSlug || roles.orderedIds.indexOf(pane.sessionId) < 0; })) return null;
|
|
17
|
+
var sessions = getCachedSessions(), driver = sessions.find(function (s) { return s.id === roles.driverId && s.sessionRole !== 'worker' && (s.ownerId || null) === (state.myUserId || null); });
|
|
18
|
+
var workers = roles.workerIds.map(function (id) { return sessions.find(function (s) { return s.id === id && s.sessionRole === 'worker' && s.parentSessionId === roles.driverId && s.parentAvailable && (s.ownerId || null) === (state.myUserId || null); }); });
|
|
19
|
+
if (!driver || !driver.sessionOriginId || workers.some(function (s) { return !s || !s.sessionOriginId; })) return null;
|
|
20
|
+
return { groupId: group.id, driverOriginId: driver.sessionOriginId, workerOrigins: workers.map(function (s) { return s.sessionOriginId; }) };
|
|
21
|
+
}
|
|
22
|
+
export function pairDisplayTree(value) { return value.linkedPair ? value.linkedPair.tree : displayTree(value); }
|
|
23
|
+
export function pairBase(pair, base) {
|
|
24
|
+
var tree = { id: pair.driverOriginId };
|
|
25
|
+
pair.workerOrigins.forEach(function (origin) { tree = { axis: 'horizontal', ratio: .5, left: tree, right: { id: origin } }; });
|
|
26
|
+
function expand(node) { if (!node) return tree; if (node.id) return node.id === pair.driverOriginId ? tree : node; return { axis: node.axis, ratio: node.ratio, left: expand(node.left), right: expand(node.right) }; }
|
|
27
|
+
return base ? expand(base) : tree;
|
|
28
|
+
}
|
|
29
|
+
export function syncPairView() {
|
|
30
|
+
var pair = currentViewPair(); if (!pair) return false;
|
|
31
|
+
var state = store.snap(), view = activeSavedView();
|
|
32
|
+
if (!view || !view.sessions.some(function (s) { return s.sessionOriginId === pair.driverOriginId; })) {
|
|
33
|
+
view = (state.projectSplitViews || []).find(function (v) { return v.sessions.some(function (s) { return s.sessionOriginId === pair.driverOriginId; }); });
|
|
34
|
+
if (view) { store.set({ projectSplitViewActiveId: view.id }); return true; }
|
|
35
|
+
if (!state.projectSplitViewPending && !state.projectSplitViewError) mutation('project_split_view_create', {
|
|
36
|
+
name: 'View', sessionOrigins: [pair.driverOriginId], tree: { id: pair.driverOriginId }, layout: 'balanced', activeSessionOriginId: pair.driverOriginId
|
|
37
|
+
});
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (!view.workspace.linkedPair || view.workspace.linkedPair.groupId !== pair.groupId || JSON.stringify(view.workspace.linkedPair.workerOrigins) !== JSON.stringify(pair.workerOrigins)) {
|
|
41
|
+
if (state.projectSplitViewPending || state.projectSplitViewError) return true;
|
|
42
|
+
if (view.sessions.length + pair.workerOrigins.length > 8) return false;
|
|
43
|
+
var value = copy(view.workspace), mapping = {};
|
|
44
|
+
value.panes.forEach(function (p) { mapping[p.kind === 'session' ? p.resource.originId : 'tool-' + (p.kind === 'workbench' ? p.resource.name : p.kind === 'file' ? 'file-' + p.instanceId : p.kind)] = p.kind === 'session' ? p.resource.originId : 'tool-' + (p.kind === 'workbench' ? p.resource.name : p.kind === 'file' ? 'file-' + p.instanceId : p.kind); });
|
|
45
|
+
function expand(node) { if (!node) return null; if (node.id) return node.id === pair.driverOriginId ? pairBase(pair) : node; return { axis: node.axis, ratio: node.ratio, left: expand(node.left), right: expand(node.right) }; }
|
|
46
|
+
value.linkedPair = Object.assign({}, pair, { tree: expand(mapWorkspaceTree(displayTree(value), mapping)) });
|
|
47
|
+
mutation('project_split_view_update', { id: view.id, sessionOrigins: view.sessions.map(function (s) { return s.sessionOriginId; }), tree: view.tree, layout: view.layout, activeSessionOriginId: pair.driverOriginId, workspace: value });
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
export function restoreViewPair(view) {
|
|
52
|
+
var pair = view && view.workspace.linkedPair;
|
|
53
|
+
if (!pair || store.get('splitPanes')) return false;
|
|
54
|
+
var group = (store.get('splitGroups') || []).find(function (item) { return item.id === pair.groupId; }), roles = splitGroupRoles(group), sessions = getCachedSessions();
|
|
55
|
+
if (!roles || roles.orderedIds.length > 8) return false;
|
|
56
|
+
var members = roles.orderedIds.map(function (id) { return sessions.find(function (s) { return s.id === id; }); });
|
|
57
|
+
if (members.some(function (s, index) { return !s || (s.ownerId || null) !== (store.get('myUserId') || null) || index > 0 && (s.sessionRole !== 'worker' || !s.parentAvailable || s.parentSessionId !== roles.driverId); }) || members[0].sessionOriginId !== pair.driverOriginId || JSON.stringify(members.slice(1).map(function (s) { return s.sessionOriginId; })) !== JSON.stringify(pair.workerOrigins)) return false;
|
|
58
|
+
store.set({ splitPanes: { groupId: group.id, panes: roles.orderedIds.map(function (id) { return splitPaneForSession(id, store.get('currentSlug'), sessions); }) } });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function pairPresentationPanes(panes) {
|
|
63
|
+
var view = activeSavedView();
|
|
64
|
+
return !view ? panes : panes.concat(view.sessions.filter(function (s) { return !panes.some(function (p) { return p.sessionId === s.id; }); }).map(function (s) { return splitPaneForSession(s.id, store.get('currentSlug'), getCachedSessions()); }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function pairViewCapacity(group) {
|
|
68
|
+
var roles = splitGroupRoles(group);
|
|
69
|
+
if (!roles) return true;
|
|
70
|
+
var view = (store.get('projectSplitViews') || []).find(function (v) { return v.sessions.some(function (s) { return s.id === roles.driverId; }); });
|
|
71
|
+
if ((view ? view.sessions.length : 1) + roles.workerIds.length <= 8) return true;
|
|
72
|
+
showToast('This View has no room for the linked Worker panes. Move a session to another View first.', 'warn');
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Pair presentation is UI state, never Worker membership or execution authority.
|
|
2
|
+
export function validLinkedPair(pair, value, view) {
|
|
3
|
+
if (!pair || Object.keys(pair).sort().join(',') !== 'driverOriginId,groupId,tree,workerOrigins' || typeof pair.groupId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(pair.groupId) ||
|
|
4
|
+
!view.sessionRefs.some(function (r) { return r.originId === pair.driverOriginId; }) || !Array.isArray(pair.workerOrigins) || !pair.workerOrigins.length || pair.workerOrigins.length + view.sessionRefs.length > 8 || new Set(pair.workerOrigins).size !== pair.workerOrigins.length || pair.workerOrigins.indexOf(pair.driverOriginId) >= 0 || pair.workerOrigins.some(function (id) { return typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id); })) return false;
|
|
5
|
+
var mapping = {}, expected = pair.workerOrigins.slice();
|
|
6
|
+
value.panes.forEach(function (p) { if (!p.open || p.placement !== 'docked') return; var id = p.kind === 'session' ? p.resource.originId : 'tool-' + (p.kind === 'workbench' ? p.resource.name : p.kind === 'file' ? 'file-' + p.instanceId : p.kind); mapping[id] = p.instanceId; expected.push(id); });
|
|
7
|
+
var count = 0, seen = [];
|
|
8
|
+
function walk(n) { if (!n || ++count > 47) return false; if (Object.keys(n).join(',') === 'id') { seen.push(n.id); return expected.indexOf(n.id) >= 0; } return Object.keys(n).sort().join(',') === 'axis,left,ratio,right' && ['horizontal','vertical'].indexOf(n.axis) >= 0 && Number.isFinite(n.ratio) && n.ratio >= .05 && n.ratio <= .95 && walk(n.left) && walk(n.right); }
|
|
9
|
+
function project(n) { if (!n) return null; if (n.id) return mapping[n.id] ? { id: mapping[n.id] } : null; var l = project(n.left), r = project(n.right); return !l ? r : !r ? l : { axis:n.axis,ratio:n.ratio,left:l,right:r }; }
|
|
10
|
+
function same(a,b) { return !a || !b ? a === b : a.id || b.id ? a.id === b.id : a.axis === b.axis && a.ratio === b.ratio && same(a.left,b.left) && same(a.right,b.right); }
|
|
11
|
+
return expected.length <= 24 && walk(pair.tree) && seen.length === expected.length && new Set(seen).size === seen.length && same(project(pair.tree),value.tree);
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { WORKBENCH_PANELS, setWorkbenchExpanded } from './workbench-panels.js';
|
|
2
|
+
import { store } from './store.js';
|
|
3
|
+
import { activeSavedView, workspaceKey, paneOwner, workspaceChanged } from './view-workspace-state.js';
|
|
4
|
+
|
|
5
|
+
export function presentation(value) { return value.presentation || { focusedSessionOriginId: null, expandedPaneId: null }; }
|
|
6
|
+
export function capturePresentation(value) {
|
|
7
|
+
var focus = store.get('projectSplitViewFocusedOrigin') || null;
|
|
8
|
+
var owner = store.get('savedViewExpandedOwner');
|
|
9
|
+
var pane = value.panes.find(function (p) { return p.open && p.kind !== 'session' && paneOwner(p) === owner; });
|
|
10
|
+
value.presentation = { focusedSessionOriginId: value.panes.some(function (p) { return p.kind === 'session' && p.resource.originId === focus; }) ? focus : null, expandedPaneId: pane ? pane.instanceId : null };
|
|
11
|
+
}
|
|
12
|
+
export function recordToolExpansion(owner, enabled) {
|
|
13
|
+
if (store.get('savedWorkspaceApplying') || !enabled && store.get('savedViewExpandedOwner') !== owner) return;
|
|
14
|
+
store.set({ savedViewExpandedOwner: enabled ? owner : null });
|
|
15
|
+
workspaceChanged();
|
|
16
|
+
}
|
|
17
|
+
export async function restoreViewPresentation(view, key) {
|
|
18
|
+
var apis = await Promise.all([import('./terminal.js'), import('./shared-browser.js'), import('./file-pane-instances.js'), import('./split-view-presentations.js')]);
|
|
19
|
+
var current = activeSavedView();
|
|
20
|
+
if (!store.get('connected') || store.get('savedWorkspaceLeaving') || workspaceKey(current) !== key || current.revision !== view.revision) return false;
|
|
21
|
+
var run = store.get('savedWorkspaceRuntime');
|
|
22
|
+
if (run && run.key === key) run.presentationApplied = true;
|
|
23
|
+
var state = presentation(view.workspace), pane = view.workspace.panes.find(function (p) { return p.instanceId === state.expandedPaneId && p.open; });
|
|
24
|
+
store.set({ savedWorkspaceApplying: true });
|
|
25
|
+
try {
|
|
26
|
+
store.set({ projectSplitViewFocusedOrigin: state.focusedSessionOriginId, savedViewExpandedOwner: pane ? paneOwner(pane) : null });
|
|
27
|
+
apis[0].setTerminalExpanded(!!pane && pane.kind === 'terminal');
|
|
28
|
+
apis[1].setBrowserExpanded(!!pane && pane.kind === 'browser');
|
|
29
|
+
Object.keys(apis[2].filePaneStates()).forEach(function (id) {
|
|
30
|
+
if (!apis[2].filePaneState(id)) return;
|
|
31
|
+
apis[2].updateFilePaneState(id, { expanded: !!pane && pane.instanceId === id }); apis[2].repaintFilePane(id);
|
|
32
|
+
});
|
|
33
|
+
Object.keys(WORKBENCH_PANELS).forEach(function (name) { setWorkbenchExpanded(name, !!pane && pane.kind === 'workbench' && pane.resource.name === name); });
|
|
34
|
+
apis[3].renderPresentationSplitView();
|
|
35
|
+
} finally { store.set({ savedWorkspaceApplying: false }); }
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { WORKBENCH_PANELS } from './workbench-panels.js';
|
|
2
|
+
import { capturePresentation, restoreViewPresentation } from './view-presentation-state.js';
|
|
3
|
+
import { boundLegacyCoordinates, legacyCoordinatesReady } from './view-legacy-coordinates.js';
|
|
4
|
+
import { currentViewPair, pairBase, pairDisplayTree } from './view-pair-presentation.js';
|
|
5
|
+
import { filePaneStates, mountFilePane, resetFilePanes } from './file-pane-instances.js';
|
|
6
|
+
import { listIds } from './split-view-layout-client.js';
|
|
7
|
+
import { store } from './store.js';
|
|
8
|
+
import { mutation } from './view-mutations.js';
|
|
9
|
+
import { activeSavedView, workspaceKey, copy, paneOwner, freshInstance, replacePaneIdentity, displayTree, fromDisplayTree, sessionTree } from './view-workspace-state.js';
|
|
10
|
+
import { workspaceWindowTree } from './tool-pane-tree.js';
|
|
11
|
+
|
|
12
|
+
function current(key) { return !store.get('savedWorkspaceLeaving') && store.get('connected') && store.get('projectSplitViewHydrated') && workspaceKey(activeSavedView()) === key; }
|
|
13
|
+
function runtime() { return store.get('savedWorkspaceRuntime'); }
|
|
14
|
+
function applying(fn) { store.set({ savedWorkspaceApplying: true }); try { return fn(); } finally { store.set({ savedWorkspaceApplying: false }); } }
|
|
15
|
+
function descriptor(name) {
|
|
16
|
+
if (name === 'terminal') return store.get('savedViewTerminalResource');
|
|
17
|
+
if (name === 'browser') {
|
|
18
|
+
var browser = store.get('sharedBrowser');
|
|
19
|
+
return browser && store.get('sessionOriginId') && { id: browser.id, runtimeId: store.get('controlRuntimeId'), sessionOriginId: store.get('sessionOriginId') };
|
|
20
|
+
}
|
|
21
|
+
var file = store.get('clayControlFileView');
|
|
22
|
+
return file && file.projectSlug === store.get('currentSlug') && { path: file.path, mode: 'follow', rendered: true };
|
|
23
|
+
}
|
|
24
|
+
function capture() {
|
|
25
|
+
var view = activeSavedView(), run = runtime();
|
|
26
|
+
if (!view || !run || run.restoring || !current(run.key) || store.get('paneMode') || store.get('savedWorkspaceApplying')) return;
|
|
27
|
+
var value = copy(run.desired || run.inflight || view.workspace), docked = store.get('rightWorkbenchDockedOwners') || [], owner = store.get('rightWorkbenchOwner');
|
|
28
|
+
['terminal', 'browser'].concat(Object.keys(WORKBENCH_PANELS)).forEach(function (name) {
|
|
29
|
+
var pane = value.panes.find(function (item) { return paneOwner(item) === name; });
|
|
30
|
+
var open = owner === name || docked.indexOf(name) >= 0;
|
|
31
|
+
if (!open && !run.restored[name] && !run.cancelled[name]) return;
|
|
32
|
+
var resource = WORKBENCH_PANELS[name] ? { name: name } : descriptor(name);
|
|
33
|
+
if (!pane && open && resource) { pane = { instanceId: freshInstance(), kind: WORKBENCH_PANELS[name] ? 'workbench' : name, open: true, placement: 'overlay', resource: resource }; value.panes.push(pane); }
|
|
34
|
+
if (!pane) return;
|
|
35
|
+
if (open) run.restored[name] = true;
|
|
36
|
+
if (open && run.cancelled[name]) { run.cancelled[name] = false; run.unavailable[name] = false; run.restored[name] = true; }
|
|
37
|
+
if (!open) run.cancelled[name] = true;
|
|
38
|
+
pane.open = open; pane.placement = open && docked.indexOf(name) >= 0 ? 'docked' : 'overlay';
|
|
39
|
+
if (open && resource && !run.unavailable[name]) {
|
|
40
|
+
if (pane.kind === 'file') resource.mode = pane.resource.mode;
|
|
41
|
+
if (pane.kind !== 'file' && JSON.stringify(pane.resource) !== JSON.stringify(resource)) replacePaneIdentity(value, pane);
|
|
42
|
+
pane.resource = resource;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
var files = filePaneStates();
|
|
46
|
+
Object.keys(files).forEach(function (id) {
|
|
47
|
+
var file = files[id], name = 'file-' + id;
|
|
48
|
+
var pane = value.panes.find(function (item) { return item.instanceId === id; });
|
|
49
|
+
if (!pane && file.open) { pane = { instanceId: id, kind: 'file', resource: file.resource }; value.panes.push(pane); }
|
|
50
|
+
if (!pane) return;
|
|
51
|
+
pane.open = file.open; pane.resource = Object.assign({}, file.resource);
|
|
52
|
+
pane.placement = docked.indexOf(name) >= 0 ? 'docked' : 'overlay';
|
|
53
|
+
run.restored[name] = true; run.cancelled[name] = !file.open;
|
|
54
|
+
});
|
|
55
|
+
// Closed file panes have no running resource and need not consume the limit.
|
|
56
|
+
value.panes = value.panes.filter(function (pane) { return pane.kind !== 'file' || pane.open; });
|
|
57
|
+
var tools = value.panes.filter(function (pane) { return pane.kind !== 'session' && pane.open && pane.placement === 'docked'; }).map(paneOwner);
|
|
58
|
+
var legacy = store.get('savedWorkspaceLegacyLayout');
|
|
59
|
+
var base = value.linkedPair ? pairBase(value.linkedPair, view.tree) : view.tree;
|
|
60
|
+
var tree = workspaceWindowTree(base, legacy && legacy.key === run.key ? boundLegacyCoordinates(legacy.record, value) : { base: base, tree: pairDisplayTree(value) }, tools);
|
|
61
|
+
if (value.linkedPair) value.linkedPair.tree = copy(tree);
|
|
62
|
+
value.tree = fromDisplayTree(value, tree);
|
|
63
|
+
var active = value.panes.find(function (pane) { return pane.open && paneOwner(pane) === owner; }) || value.panes.find(function (pane) { return pane.open && pane.instanceId === value.activePaneId; }) || value.panes.find(function (pane) { return pane.open; });
|
|
64
|
+
value.activePaneId = active ? active.instanceId : null;
|
|
65
|
+
if (run.presentationApplied) capturePresentation(value);
|
|
66
|
+
store.set({ savedWorkspaceRuntime: Object.assign({}, run, { desired: value, desiredBase: JSON.stringify(run.inflight || view.workspace) }) });
|
|
67
|
+
queue();
|
|
68
|
+
}
|
|
69
|
+
export function persistWorkspace(value) {
|
|
70
|
+
var view = activeSavedView(), run = runtime();
|
|
71
|
+
if (!view || !run || !current(run.key)) return false;
|
|
72
|
+
store.set({ savedWorkspaceLegacyLayout: null });
|
|
73
|
+
run.migrated = true;
|
|
74
|
+
store.set({ savedWorkspaceRuntime: Object.assign({}, run, { desired: copy(value), desiredBase: JSON.stringify(run.inflight || view.workspace) }) });
|
|
75
|
+
flush(); return true;
|
|
76
|
+
}
|
|
77
|
+
function flush() {
|
|
78
|
+
var view = activeSavedView(), run = runtime();
|
|
79
|
+
if (!view || !run || !current(run.key) || run.restoring || run.presentationApplying || !run.desired || store.get('projectSplitViewPending') || store.get('projectSplitViewError')) return;
|
|
80
|
+
if (view.workspace.panes.some(function(pane) { var name = paneOwner(pane); return pane.kind !== 'session' && pane.open && !run.restored[name] && !run.cancelled[name]; })) return;
|
|
81
|
+
if (run.desiredBase !== undefined && run.desiredBase !== JSON.stringify(view.workspace)) {
|
|
82
|
+
store.set({ savedWorkspaceRuntime: Object.assign({}, run, { desired: null, inflight: null }), projectSplitViewError: 'The saved workspace changed while this edit was pending. Reload before trying again.' }); return;
|
|
83
|
+
}
|
|
84
|
+
if (JSON.stringify(run.desired) === JSON.stringify(view.workspace) && store.get('projectSplitViewSavedActiveId') === view.id) { store.set({ savedWorkspaceRuntime: Object.assign({}, run, { desired: null }) }); return; }
|
|
85
|
+
var tree = sessionTree(run.desired), origins = view.sessions.map(function (session) { return session.sessionOriginId; });
|
|
86
|
+
var sent = run.desired;
|
|
87
|
+
store.set({ savedWorkspaceRuntime: Object.assign({}, run, { desired: null, inflight: copy(sent) }) });
|
|
88
|
+
if (!mutation('project_split_view_update', { id: view.id, sessionOrigins: origins, tree: tree, layout: view.layout,
|
|
89
|
+
activeSessionOriginId: view.activeSessionOriginId, workspace: sent })) store.set({ savedWorkspaceRuntime: Object.assign({}, runtime(), { desired: sent, inflight: null }) });
|
|
90
|
+
}
|
|
91
|
+
function restoreCurrent(view, run) {
|
|
92
|
+
if (!current(run.key)) return false;
|
|
93
|
+
if (activeSavedView().revision === view.revision) return true;
|
|
94
|
+
store.set({ savedWorkspaceRuntime: Object.assign({}, runtime(), { restoring: false }), projectSplitViewError: 'The View changed during restoration. Reload to restore its latest state.' });
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
async function restore(view, run) {
|
|
98
|
+
var workbench = await import('./right-workbench.js');
|
|
99
|
+
if (!restoreCurrent(view, run) || runtime() !== run) return;
|
|
100
|
+
store.set({ savedViewExpandedOwner: null });
|
|
101
|
+
applying(function () { resetFilePanes(); workbench.resetRightWorkbench(); });
|
|
102
|
+
for (var i = 0; i < view.workspace.panes.length; i++) {
|
|
103
|
+
var pane = view.workspace.panes[i], name = paneOwner(pane);
|
|
104
|
+
if (pane.kind === 'session' || !pane.open) continue;
|
|
105
|
+
var api = pane.kind === 'workbench' ? await WORKBENCH_PANELS[name].load() : await import(pane.kind === 'terminal' ? './terminal.js' : pane.kind === 'browser' ? './shared-browser.js' : './filebrowser.js');
|
|
106
|
+
if (!restoreCurrent(view, run) || !runtime() || runtime().key !== run.key) return;
|
|
107
|
+
if (runtime().cancelled[name]) continue;
|
|
108
|
+
var result = applying(function () {
|
|
109
|
+
if (pane.kind === 'workbench') {
|
|
110
|
+
api[WORKBENCH_PANELS[name].open]();
|
|
111
|
+
var panel = document.getElementById(WORKBENCH_PANELS[name].id);
|
|
112
|
+
return panel && !panel.classList.contains('hidden') ? 'restored' : 'unavailable';
|
|
113
|
+
}
|
|
114
|
+
if (pane.kind === 'terminal') return api.restoreExistingTerminal(pane.resource);
|
|
115
|
+
if (pane.kind === 'browser') return api.restoreExistingBrowser(pane.resource);
|
|
116
|
+
return mountFilePane(pane);
|
|
117
|
+
});
|
|
118
|
+
if (result === 'pending') continue;
|
|
119
|
+
run = runtime(); run.restored[name] = true; run.unavailable[name] = result === 'unavailable';
|
|
120
|
+
if (pane.placement === 'docked' && (pane.kind !== 'workbench' || result !== 'unavailable')) applying(function () { workbench.restoreWorkbenchDock(name); });
|
|
121
|
+
}
|
|
122
|
+
if (!current(run.key)) return;
|
|
123
|
+
store.set({ savedWorkspaceRuntime: Object.assign({}, runtime(), { restoring: false }) });
|
|
124
|
+
queue();
|
|
125
|
+
}
|
|
126
|
+
async function retryPending(view, run, name) {
|
|
127
|
+
var pane = view.workspace.panes.find(function (item) { return item.kind === name && item.open; });
|
|
128
|
+
var ready = store.get(name === 'terminal' ? 'savedViewTerminalListReady' : 'savedViewBrowserListReady');
|
|
129
|
+
if (!pane || run.restored[name] || run.cancelled[name] || !ready) return;
|
|
130
|
+
var api = await import(name === 'terminal' ? './terminal.js' : './shared-browser.js'), workbench = await import('./right-workbench.js');
|
|
131
|
+
if (!restoreCurrent(view, run) || runtime().cancelled[name] || runtime().restored[name]) return;
|
|
132
|
+
var result = applying(function () { return name === 'terminal' ? api.restoreExistingTerminal(pane.resource) : api.restoreExistingBrowser(pane.resource); });
|
|
133
|
+
if (result === 'pending') return;
|
|
134
|
+
runtime().restored[name] = true; runtime().unavailable[name] = result === 'unavailable';
|
|
135
|
+
if (pane.placement === 'docked') applying(function () { workbench.restoreWorkbenchDock(name); });
|
|
136
|
+
queue();
|
|
137
|
+
}
|
|
138
|
+
function migrateCoordinates(view, run) {
|
|
139
|
+
var preference = store.get('workbenchLayoutPreference');
|
|
140
|
+
if (run.migrated || !preference || !preference.ready || run.restoring || store.get('projectSplitViewPending')) return;
|
|
141
|
+
// Legacy coordinate records are read once; subsequent writes target the View.
|
|
142
|
+
if (!run.legacyEligible) { run.migrated = true; return; }
|
|
143
|
+
var pair = view.workspace.linkedPair;
|
|
144
|
+
var scopes = [view.id].concat(pair ? ['legacy-' + pair.groupId] : []).concat(view.sessions.length === 1 ? [view.sessions[0].sessionOriginId] : []), record;
|
|
145
|
+
scopes.some(function (scope) { record = (preference.layouts || {})[JSON.stringify([store.get('currentSlug'), store.get('currentProjectOwnerId') || null, scope])]; return !!record; });
|
|
146
|
+
if (!record) { run.migrated = true; return; }
|
|
147
|
+
// Coordinates alone cannot authorize opening tools or invent their bindings.
|
|
148
|
+
store.set({ savedWorkspaceLegacyLayout: { key: run.key, record: copy(record) } });
|
|
149
|
+
var owners = store.get('rightWorkbenchDockedOwners') || [];
|
|
150
|
+
var waiting = listIds(record.tree).filter(function (id) { return id.indexOf('tool-') === 0; });
|
|
151
|
+
if (waiting.indexOf('tool-file-viewer') >= 0 && !legacyCoordinatesReady(record, view.workspace)) return;
|
|
152
|
+
if (!waiting.filter(function (id) { return id !== 'tool-file-viewer'; }).every(function (id) { return owners.indexOf(id.slice(5)) >= 0 && (id === 'tool-terminal' ? descriptor('terminal') : id === 'tool-browser' ? descriptor('browser') : true); })) return;
|
|
153
|
+
run.migrated = true; capture();
|
|
154
|
+
}
|
|
155
|
+
function tick() {
|
|
156
|
+
if (store.get('paneMode') || store.get('splitPanes') && !currentViewPair()) return;
|
|
157
|
+
var view = activeSavedView();
|
|
158
|
+
if (store.get('splitPanes') && (!view || !view.workspace.linkedPair || store.get('projectSplitViewPending'))) return;
|
|
159
|
+
if (!view || !store.get('connected') || !store.get('projectSplitViewHydrated')) return;
|
|
160
|
+
if (view.sessions.length && (store.get('sessionOriginId') !== view.activeSessionOriginId || store.get('sessionActivatedProjectSlug') !== store.get('currentSlug'))) return;
|
|
161
|
+
var key = workspaceKey(view), run = runtime();
|
|
162
|
+
if (!run || run.key !== key) {
|
|
163
|
+
run = { key: key, appliedWorkspace: JSON.stringify(view.workspace), restoring: true, restored: {}, cancelled: {}, unavailable: {}, desired: null, migrated: false, legacyEligible: !view.workspace.panes.some(function (pane) { return pane.kind !== 'session'; }) };
|
|
164
|
+
store.set({ savedWorkspaceRuntime: run, savedWorkspaceLegacyLayout: null });
|
|
165
|
+
restore(view, run); return;
|
|
166
|
+
}
|
|
167
|
+
var savedWorkspace = JSON.stringify(view.workspace);
|
|
168
|
+
if (!run.restoring && run.appliedWorkspace && run.appliedWorkspace !== savedWorkspace && !store.get('projectSplitViewPending')) {
|
|
169
|
+
if (run.inflight && JSON.stringify(run.inflight) === savedWorkspace) run.appliedWorkspace = savedWorkspace;
|
|
170
|
+
else if (run.desired || run.inflight) {
|
|
171
|
+
store.set({ projectSplitViewError: 'The saved workspace changed while a local edit was pending. Reload before trying again.' }); return;
|
|
172
|
+
} else {
|
|
173
|
+
run = { key: key, appliedWorkspace: savedWorkspace, restoring: true, restored: {}, cancelled: {}, unavailable: {}, desired: null, migrated: true, legacyEligible: false };
|
|
174
|
+
store.set({ savedWorkspaceRuntime: run, savedWorkspaceLegacyLayout: null }); restore(view, run); return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
var legacy = store.get('savedWorkspaceLegacyLayout');
|
|
178
|
+
if (legacy && run.migrated && legacyCoordinatesReady(legacy.record, view.workspace)) store.set({ savedWorkspaceLegacyLayout: null });
|
|
179
|
+
if (!run.restoring) {
|
|
180
|
+
if (!run.presentationApplied && !run.presentationApplying && !view.workspace.panes.some(function (p) { return p.kind !== 'session' && p.open && !run.restored[paneOwner(p)] && !run.cancelled[paneOwner(p)]; })) {
|
|
181
|
+
run.presentationApplying = true;
|
|
182
|
+
restoreViewPresentation(view, run.key).then(function (applied) { if (runtime() && runtime().key === run.key && runtime().appliedWorkspace === run.appliedWorkspace) { runtime().presentationApplied = applied; runtime().presentationApplying = false; queue(); } });
|
|
183
|
+
}
|
|
184
|
+
retryPending(view, run, 'terminal'); retryPending(view, run, 'browser'); migrateCoordinates(view, run);
|
|
185
|
+
if (!store.get('projectSplitViewPending')) run.inflight = null;
|
|
186
|
+
if (!run.desired && store.get('projectSplitViewSavedActiveId') !== view.id) { run.desired = copy(view.workspace); run.desiredBase = JSON.stringify(view.workspace); }
|
|
187
|
+
flush();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function queue() {
|
|
191
|
+
if (store.get('savedWorkspaceTickQueued')) return;
|
|
192
|
+
store.set({ savedWorkspaceTickQueued: true });
|
|
193
|
+
queueMicrotask(function () { store.set({ savedWorkspaceTickQueued: false }); tick(); });
|
|
194
|
+
}
|
|
195
|
+
export function initSavedWorkspace() {
|
|
196
|
+
if (store.get('savedWorkspaceInitialized')) return;
|
|
197
|
+
store.set({ savedWorkspaceInitialized: true });
|
|
198
|
+
window.addEventListener('beforeunload', function () { store.set({ savedWorkspaceLeaving: true }); });
|
|
199
|
+
window.addEventListener('pagehide', function () { store.set({ savedWorkspaceLeaving: true }); });
|
|
200
|
+
document.addEventListener('clay-workspace-changed', capture);
|
|
201
|
+
store.subscribe(function (state, previous) {
|
|
202
|
+
if (previous.savedViewBrowserListReady && !state.savedViewBrowserListReady && runtime()) runtime().restored.browser = false;
|
|
203
|
+
if ((state.savedViewTerminalResource !== previous.savedViewTerminalResource && state.savedViewTerminalResource || state.clayControlFileView !== previous.clayControlFileView && state.clayControlFileView || state.sharedBrowser !== previous.sharedBrowser && state.sharedBrowser) && !state.savedWorkspaceApplying) capture();
|
|
204
|
+
if (state.splitPanes !== previous.splitPanes || state.rightWorkbenchDockedOwners !== previous.rightWorkbenchDockedOwners || state.savedViewTerminalResource !== previous.savedViewTerminalResource || state.projectSplitViews !== previous.projectSplitViews || state.projectSplitViewActiveId !== previous.projectSplitViewActiveId ||
|
|
205
|
+
state.projectSplitViewPending !== previous.projectSplitViewPending || state.connected !== previous.connected ||
|
|
206
|
+
state.controlRuntimeId !== previous.controlRuntimeId || state.sharedBrowsers !== previous.sharedBrowsers ||
|
|
207
|
+
state.sessionOriginId !== previous.sessionOriginId || state.sessionActivatedProjectSlug !== previous.sessionActivatedProjectSlug || state.savedViewBrowserListReady !== previous.savedViewBrowserListReady || state.savedViewTerminalListReady !== previous.savedViewTerminalListReady || state.workbenchLayoutPreference !== previous.workbenchLayoutPreference) queue();
|
|
208
|
+
});
|
|
209
|
+
queue();
|
|
210
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { listIds } from './split-view-layout-client.js';
|
|
3
|
+
|
|
4
|
+
export function activeSavedView() { return (store.get('projectSplitViews') || []).find(function (view) { return view.id === store.get('projectSplitViewActiveId'); }) || null; }
|
|
5
|
+
export function workspaceKey(view) {
|
|
6
|
+
return view && JSON.stringify([store.get('myUserId') || null, store.get('currentSlug'), store.get('currentProjectOwnerId'), store.get('projectSplitViewSocketGeneration'), view.id]);
|
|
7
|
+
}
|
|
8
|
+
export function copy(value) { return JSON.parse(JSON.stringify(value)); }
|
|
9
|
+
export function mapWorkspaceTree(tree, mapping) {
|
|
10
|
+
if (!tree) return null;
|
|
11
|
+
if (tree.id) return mapping[tree.id] ? { id: mapping[tree.id] } : null;
|
|
12
|
+
var left = mapWorkspaceTree(tree.left, mapping), right = mapWorkspaceTree(tree.right, mapping);
|
|
13
|
+
return !left ? right : !right ? left : { axis: tree.axis, ratio: tree.ratio, left: left, right: right };
|
|
14
|
+
}
|
|
15
|
+
export function paneOwner(pane) { return pane.kind === 'workbench' ? pane.resource.name : pane.kind === 'file' ? 'file-' + pane.instanceId : pane.kind; }
|
|
16
|
+
export function displayTree(value) {
|
|
17
|
+
var mapping = Object.create(null);
|
|
18
|
+
value.panes.forEach(function (pane) { mapping[pane.instanceId] = pane.kind === 'session' ? pane.resource.originId : 'tool-' + paneOwner(pane); });
|
|
19
|
+
return mapWorkspaceTree(value.tree, mapping);
|
|
20
|
+
}
|
|
21
|
+
export function sessionTree(value) {
|
|
22
|
+
var mapping = Object.create(null);
|
|
23
|
+
value.panes.forEach(function (pane) { if (pane.kind === 'session') mapping[pane.instanceId] = pane.resource.originId; });
|
|
24
|
+
return mapWorkspaceTree(value.tree, mapping);
|
|
25
|
+
}
|
|
26
|
+
export function fromDisplayTree(value, tree) {
|
|
27
|
+
var mapping = Object.create(null);
|
|
28
|
+
value.panes.forEach(function (pane) { mapping[pane.kind === 'session' ? pane.resource.originId : 'tool-' + paneOwner(pane)] = pane.instanceId; });
|
|
29
|
+
return mapWorkspaceTree(tree, mapping);
|
|
30
|
+
}
|
|
31
|
+
export function replacePaneIdentity(value, pane) {
|
|
32
|
+
var previous = pane.instanceId, next = freshInstance(), mapping = Object.create(null);
|
|
33
|
+
value.panes.forEach(function(item) { mapping[item.instanceId] = item.instanceId === previous ? next : item.instanceId; });
|
|
34
|
+
value.tree = mapWorkspaceTree(value.tree, mapping);
|
|
35
|
+
if (value.activePaneId === previous) value.activePaneId = next;
|
|
36
|
+
pane.instanceId = next;
|
|
37
|
+
}
|
|
38
|
+
export function freshInstance() { return 'pane-' + crypto.randomUUID(); }
|
|
39
|
+
// Keep tool branches and their ratios while replacing the session layout.
|
|
40
|
+
export function recomposeWorkspace(previous, tree, activeOrigin) {
|
|
41
|
+
var next = copy(previous || { version: 1, panes: [], tree: null, activePaneId: null }), origins = listIds(tree), oldTree = displayTree(next);
|
|
42
|
+
next.panes = next.panes.filter(function (pane) { return pane.kind !== 'session' || origins.indexOf(pane.resource.originId) >= 0; });
|
|
43
|
+
origins.forEach(function (origin) {
|
|
44
|
+
if (!next.panes.some(function (pane) { return pane.kind === 'session' && pane.resource.originId === origin; })) next.panes.push({ instanceId: freshInstance(), kind: 'session', open: true, placement: 'docked', resource: { originId: origin } });
|
|
45
|
+
});
|
|
46
|
+
if (next.linkedPair && (origins.indexOf(next.linkedPair.driverOriginId) < 0 || origins.length + next.linkedPair.workerOrigins.length > 8)) delete next.linkedPair;
|
|
47
|
+
var placements = [];
|
|
48
|
+
function collect(node) {
|
|
49
|
+
if (!node || node.id) return;
|
|
50
|
+
if (node.left.id && node.left.id.indexOf('tool-') === 0) placements.push({ id: node.left.id, target: listIds(node.right)[0], axis: node.axis, ratio: node.ratio, before: true });
|
|
51
|
+
else if (node.right.id && node.right.id.indexOf('tool-') === 0) placements.push({ id: node.right.id, target: listIds(node.left)[0], axis: node.axis, ratio: node.ratio, before: false });
|
|
52
|
+
collect(node.left); collect(node.right);
|
|
53
|
+
}
|
|
54
|
+
collect(oldTree);
|
|
55
|
+
var result = tree;
|
|
56
|
+
function insert(node, placement) {
|
|
57
|
+
if (!node) return node;
|
|
58
|
+
if (node.id) return node.id === placement.target ? { axis: placement.axis, ratio: placement.ratio,
|
|
59
|
+
left: placement.before ? { id: placement.id } : node, right: placement.before ? node : { id: placement.id } } : node;
|
|
60
|
+
return { axis: node.axis, ratio: node.ratio, left: insert(node.left, placement), right: insert(node.right, placement) };
|
|
61
|
+
}
|
|
62
|
+
placements.reverse().forEach(function (placement) { result = insert(result, placement); });
|
|
63
|
+
next.panes.filter(function (pane) { return pane.kind !== 'session' && pane.open && pane.placement === 'docked'; }).forEach(function (pane) {
|
|
64
|
+
var id = 'tool-' + paneOwner(pane);
|
|
65
|
+
if (listIds(result).indexOf(id) < 0) result = result ? { axis: 'horizontal', ratio: .66, left: result, right: { id: id } } : { id: id };
|
|
66
|
+
});
|
|
67
|
+
next.tree = fromDisplayTree(next, result);
|
|
68
|
+
var selected = next.panes.find(function (pane) { return pane.kind === 'session' && pane.resource.originId === activeOrigin; }) || next.panes.find(function (pane) { return pane.open; });
|
|
69
|
+
next.activePaneId = selected ? selected.instanceId : null;
|
|
70
|
+
if (next.presentation) {
|
|
71
|
+
if (origins.indexOf(next.presentation.focusedSessionOriginId) < 0) next.presentation.focusedSessionOriginId = null;
|
|
72
|
+
if (!next.panes.some(function (p) { return p.open && p.instanceId === next.presentation.expandedPaneId; })) next.presentation.expandedPaneId = null;
|
|
73
|
+
}
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
export function prepareViewMutation(type, message) {
|
|
77
|
+
var state = store.snap(), views = state.projectSplitViews || [];
|
|
78
|
+
var view = views.find(function (item) { return item.id === (message.id || message.sourceViewId); });
|
|
79
|
+
message.accountId = state.myUserId || null; message.projectSlug = state.currentSlug;
|
|
80
|
+
if (view) message.expectedViewRevision = view.revision;
|
|
81
|
+
if (type === 'project_split_view_create' || type === 'project_split_view_update') {
|
|
82
|
+
if (!message.workspace) message.workspace = recomposeWorkspace(view && view.workspace, message.tree, message.activeSessionOriginId);
|
|
83
|
+
message.tree = sessionTree(message.workspace);
|
|
84
|
+
message.active = true;
|
|
85
|
+
}
|
|
86
|
+
if (type === 'project_split_view_move') {
|
|
87
|
+
var target = views.find(function (item) { return item.id === message.targetViewId; });
|
|
88
|
+
message.expectedTargetViewRevision = target && target.revision;
|
|
89
|
+
message.targetWorkspace = recomposeWorkspace(target && target.workspace, message.tree, message.activeSessionOriginId);
|
|
90
|
+
var moved = view && view.workspace.panes.find(function (pane) { return pane.kind === 'session' && pane.resource.originId === message.sourceSessionOriginId; });
|
|
91
|
+
var destination = message.targetWorkspace.panes.find(function (pane) { return pane.kind === 'session' && pane.resource.originId === message.sourceSessionOriginId; });
|
|
92
|
+
if (moved && destination) {
|
|
93
|
+
var mapping = Object.create(null); message.targetWorkspace.panes.forEach(function (pane) { mapping[pane.instanceId] = pane === destination ? moved.instanceId : pane.instanceId; });
|
|
94
|
+
message.targetWorkspace.tree = mapWorkspaceTree(message.targetWorkspace.tree, mapping);
|
|
95
|
+
if (message.targetWorkspace.activePaneId === destination.instanceId) message.targetWorkspace.activePaneId = moved.instanceId;
|
|
96
|
+
destination.instanceId = moved.instanceId;
|
|
97
|
+
}
|
|
98
|
+
message.tree = sessionTree(message.targetWorkspace);
|
|
99
|
+
message.active = true;
|
|
100
|
+
}
|
|
101
|
+
return message;
|
|
102
|
+
}
|
|
103
|
+
export function workspaceChanged() { document.dispatchEvent(new CustomEvent('clay-workspace-changed')); }
|