clay-server 5.3.0-beta.5 → 5.3.0-beta.7
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/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-admin-overview.js +22 -11
- package/lib/project-background-tasks.js +76 -0
- package/lib/project-connection.js +1 -1
- 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-sessions.js +12 -5
- package/lib/project-shared-browser.js +7 -3
- package/lib/project-split-view-document.js +16 -2
- package/lib/project-split-views.js +27 -297
- 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 +348 -0
- package/lib/project-view-terminal-binding.js +16 -0
- package/lib/project-view-workspace.js +126 -0
- package/lib/project-worker-conversation.js +5 -0
- package/lib/project.js +1 -0
- package/lib/public/app.js +9 -0
- package/lib/public/css/contextual-clay.css +2 -1
- package/lib/public/css/issues.css +3 -0
- package/lib/public/css/mate-conversation.css +1 -1
- package/lib/public/css/pane.css +5 -1
- package/lib/public/css/pending-message-queue.css +24 -19
- package/lib/public/css/project-admin-overview.css +42 -13
- package/lib/public/css/right-workbench.css +0 -1
- package/lib/public/css/scheduled-tasks.css +7 -0
- package/lib/public/css/shared-browser.css +31 -8
- package/lib/public/css/split-view-layout.css +129 -10
- package/lib/public/css/tool-pane.css +124 -0
- package/lib/public/index.html +6 -6
- package/lib/public/modules/agent-message-bubbles.js +96 -0
- package/lib/public/modules/app-header.js +6 -0
- package/lib/public/modules/app-messages.js +26 -9
- package/lib/public/modules/background-task-previews.js +8 -0
- package/lib/public/modules/background-task-viewer.js +97 -0
- package/lib/public/modules/background-tasks-ui.js +34 -125
- package/lib/public/modules/chat-bubble-renderer.js +11 -5
- package/lib/public/modules/clay-file-links.js +2 -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/contextual-clay-geometry.js +7 -3
- 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 +43 -82
- 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 +7 -3
- package/lib/public/modules/pane-binding.js +5 -1
- package/lib/public/modules/pane-browser-bridge.js +6 -2
- package/lib/public/modules/pane-browser-transport.js +110 -0
- package/lib/public/modules/pane-header-tooltips.js +32 -0
- package/lib/public/modules/pending-message-queue-drag.js +69 -0
- package/lib/public/modules/pending-message-queue.js +3 -33
- package/lib/public/modules/permission-control.js +6 -5
- package/lib/public/modules/plantuml-preview.js +12 -12
- package/lib/public/modules/project-admin-overview.js +125 -10
- package/lib/public/modules/project-grouping.js +9 -0
- package/lib/public/modules/right-workbench.js +34 -15
- package/lib/public/modules/scheduled-tasks.js +7 -6
- package/lib/public/modules/server-settings-controls.js +33 -4
- package/lib/public/modules/session-folder-toolbar.js +1 -1
- package/lib/public/modules/session-tool-item.js +86 -0
- package/lib/public/modules/settings-select.js +2 -1
- package/lib/public/modules/shared-browser-tabs.js +37 -21
- package/lib/public/modules/shared-browser.js +69 -9
- package/lib/public/modules/sidebar-projects.js +1 -10
- package/lib/public/modules/split-pane-renderer.js +1 -0
- package/lib/public/modules/split-session-empty-pane.js +47 -0
- package/lib/public/modules/split-view-create-menu.js +74 -65
- 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-layout-scope.js +4 -0
- package/lib/public/modules/split-view-picker-actions.js +71 -0
- package/lib/public/modules/split-view-picker-render.js +81 -0
- 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 +22 -17
- package/lib/public/modules/split-view-presentations.js +25 -33
- package/lib/public/modules/split-view-worker-activity.js +2 -2
- package/lib/public/modules/split-view-worker-transcript.js +70 -33
- package/lib/public/modules/split-view-workers.js +6 -4
- package/lib/public/modules/split-view.js +9 -2
- package/lib/public/modules/sticky-notes-browser.js +2 -2
- package/lib/public/modules/terminal-viewport.js +14 -0
- package/lib/public/modules/terminal.js +49 -23
- package/lib/public/modules/theme-palette.js +177 -0
- package/lib/public/modules/theme.js +2 -180
- package/lib/public/modules/tool-pane-chrome.js +88 -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/tools.js +5 -89
- package/lib/public/modules/tooltip.js +32 -6
- 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 +13 -2
- package/lib/public/modules/workbench-layout-preference.js +12 -5
- package/lib/public/modules/workbench-panels.js +21 -0
- package/lib/public/modules/worker-message-previews.js +39 -0
- package/lib/public/pane-assets/app-5W6KJXK5.js +1 -0
- package/lib/public/pane-assets/chunk-2IJUTAB6.js +2 -0
- package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
- package/lib/public/pane-assets/chunk-7OVNC5HT.js +1 -0
- package/lib/public/pane-assets/chunk-BQN7B62H.js +3 -0
- package/lib/public/pane-assets/chunk-EMLN2Y7L.js +1 -0
- package/lib/public/pane-assets/chunk-ENPAHXLB.js +1 -0
- package/lib/public/pane-assets/chunk-EPBGGZRT.js +1 -0
- package/lib/public/pane-assets/chunk-EYTE56IA.js +1 -0
- package/lib/public/pane-assets/chunk-FQTNR4ZS.js +1 -0
- package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
- package/lib/public/pane-assets/chunk-L6LSA5WI.js +1 -0
- package/lib/public/pane-assets/chunk-M57UIGZV.js +1 -0
- package/lib/public/pane-assets/chunk-MGTFTEJP.js +1 -0
- package/lib/public/pane-assets/chunk-MMG6MYCN.js +1 -0
- package/lib/public/pane-assets/chunk-MZLMEXMW.js +1 -0
- package/lib/public/pane-assets/chunk-OG3E5SVA.js +166 -0
- package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
- package/lib/public/pane-assets/chunk-RGAFZ3T2.js +1 -0
- package/lib/public/pane-assets/chunk-RNDRURMF.js +1 -0
- package/lib/public/pane-assets/chunk-SJOT74VW.js +2 -0
- package/lib/public/pane-assets/chunk-SJTGHIN5.js +1 -0
- package/lib/public/pane-assets/chunk-SLMAFKI2.js +1 -0
- package/lib/public/pane-assets/chunk-U3I5KY3B.js +1 -0
- package/lib/public/pane-assets/chunk-UPXU2IPC.js +1 -0
- package/lib/public/pane-assets/chunk-VEBYD7TL.js +1 -0
- package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
- package/lib/public/pane-assets/chunk-XBHYNXXT.js +1 -0
- package/lib/public/pane-assets/manifest.json +438 -381
- package/lib/public/pane-assets/style-BSKKK54Q.css +1 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-message-processor.js +5 -2
- package/lib/server-storage.js +27 -3
- package/lib/server-workbench-layout-settings.js +3 -3
- package/lib/server.js +4 -2
- package/lib/sessions.js +1 -1
- package/lib/split-view-layout.js +11 -7
- package/lib/storage-usage.js +29 -7
- package/lib/terminal-manager.js +2 -2
- package/lib/users-workbench-layout-preferences.js +14 -4
- package/lib/workbench-window-layout.js +30 -0
- package/lib/worker-message-previews.js +50 -0
- package/lib/ws-schema.js +1 -1
- package/lib/yoke/adapters/claude-worker.js +8 -0
- package/lib/yoke/adapters/claude.js +17 -2
- package/lib/yoke/claude-task-reader.js +94 -0
- package/lib/yoke/interface.js +1 -0
- package/package.json +1 -1
- package/lib/public/pane-assets/app-6H5S2Y3Z.js +0 -1
- package/lib/public/pane-assets/chunk-3QV5SDC4.js +0 -1
- package/lib/public/pane-assets/chunk-4FM3U3DW.js +0 -1
- package/lib/public/pane-assets/chunk-7GZH3FZY.js +0 -1
- package/lib/public/pane-assets/chunk-AGJTF5WQ.js +0 -1
- 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-NPKRTUNG.js +0 -2
- package/lib/public/pane-assets/chunk-VGA2Y3D2.js +0 -2
- package/lib/public/pane-assets/chunk-WKO3T75E.js +0 -167
- package/lib/public/pane-assets/style-TSRDU54Z.css +0 -1
|
@@ -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')); }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { WORKBENCH_PANELS } from './workbench-panels.js';
|
|
2
|
+
import { validLinkedPair } from './view-pair-validation.js';
|
|
3
|
+
import { sessionTree } from './view-workspace-state.js';
|
|
4
|
+
function id(value) { return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value); }
|
|
5
|
+
function runtime(value) { return id(value) || typeof value === 'string' && /^runtime:[A-Za-z0-9_-]{1,128}$/.test(value); }
|
|
6
|
+
function fields(value, names) { return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).sort().join(',') === names.split(',').sort().join(','); }
|
|
7
|
+
function sameTree(a, b) { return !a || !b ? a === b : a.id || b.id ? a.id === b.id : a.axis === b.axis && a.ratio === b.ratio && sameTree(a.left,b.left) && sameTree(a.right,b.right); }
|
|
8
|
+
export function validWorkspaceView(view) {
|
|
9
|
+
var value = view && view.workspace;
|
|
10
|
+
if (!value || !fields(value, 'version,panes,tree,activePaneId' + (value.linkedPair === undefined ? '' : ',linkedPair') + (value.presentation === undefined ? '' : ',presentation')) || value.version !== 1 || !Array.isArray(value.panes) || value.panes.length > 24) return false;
|
|
11
|
+
var instances = [], origins = [], docked = [], open = [], resources = [], tools = 0, files = 0, overlays = 0;
|
|
12
|
+
for (var i = 0; i < value.panes.length; i++) {
|
|
13
|
+
var p = value.panes[i], r = p && p.resource;
|
|
14
|
+
if (!fields(p, 'instanceId,kind,open,placement,resource') || !id(p.instanceId) || instances.indexOf(p.instanceId) >= 0 || typeof p.open !== 'boolean' || ['docked', 'overlay'].indexOf(p.placement) < 0) return false;
|
|
15
|
+
instances.push(p.instanceId);
|
|
16
|
+
if (p.kind === 'session') {
|
|
17
|
+
if (!fields(r, 'originId') || !id(r.originId) || origins.indexOf(r.originId) >= 0 || !p.open || p.placement !== 'docked') return false;
|
|
18
|
+
origins.push(r.originId);
|
|
19
|
+
} else {
|
|
20
|
+
if (++tools > 16) return false;
|
|
21
|
+
if (p.kind === 'file') {
|
|
22
|
+
if (++files > 8) return false;
|
|
23
|
+
if (!fields(r, 'path,mode,rendered') || typeof r.path !== 'string' || !r.path || r.path.length > 4096 || /[\\\x00-\x1f\x7f:]/.test(r.path) || !r.path.split('/').every(function(part) { return part && part !== '.' && part !== '..'; }) || ['follow', 'pin'].indexOf(r.mode) < 0 || typeof r.rendered !== 'boolean') return false;
|
|
24
|
+
} else if (p.kind === 'terminal' || p.kind === 'browser') {
|
|
25
|
+
if (!runtime(r && r.runtimeId)) return false;
|
|
26
|
+
if (p.kind === 'terminal' ? !fields(r, 'id,identity,runtimeId') || !Number.isSafeInteger(r.id) || r.id < 1 || !id(r.identity) : !fields(r, 'id,runtimeId,sessionOriginId') || !id(r.id) || !id(r.sessionOriginId)) return false;
|
|
27
|
+
var key = p.kind + ':' + r.runtimeId + ':' + r.id;
|
|
28
|
+
if (resources.indexOf(key) >= 0) return false;
|
|
29
|
+
resources.push(key);
|
|
30
|
+
} else if (p.kind === 'workbench') {
|
|
31
|
+
if (!fields(r, 'name') || !Object.prototype.hasOwnProperty.call(WORKBENCH_PANELS, r.name) || resources.indexOf('workbench:' + r.name) >= 0) return false;
|
|
32
|
+
resources.push('workbench:' + r.name);
|
|
33
|
+
} else return false;
|
|
34
|
+
}
|
|
35
|
+
if (p.open) { open.push(p.instanceId); if (p.placement === 'docked') docked.push(p.instanceId); else overlays++; }
|
|
36
|
+
}
|
|
37
|
+
if (origins.length !== view.sessions.length || view.sessions.some(function(s) { return origins.indexOf(s.sessionOriginId) < 0; }) || overlays > 1 || (open.length ? open.indexOf(value.activePaneId) < 0 : value.activePaneId !== null)) return false;
|
|
38
|
+
var seen = [], nodes = 0;
|
|
39
|
+
function walk(node) {
|
|
40
|
+
if (++nodes > 47) return false;
|
|
41
|
+
if (fields(node, 'id')) { if (seen.indexOf(node.id) >= 0 || docked.indexOf(node.id) < 0) return false; seen.push(node.id); return true; }
|
|
42
|
+
return fields(node, 'axis,ratio,left,right') && ['horizontal', 'vertical'].indexOf(node.axis) >= 0 && Number.isFinite(node.ratio) && node.ratio >= .05 && node.ratio <= .95 && walk(node.left) && walk(node.right);
|
|
43
|
+
}
|
|
44
|
+
if (docked.length ? !walk(value.tree) || seen.length !== docked.length : value.tree !== null) return false;
|
|
45
|
+
if (value.presentation !== undefined && (!fields(value.presentation, 'focusedSessionOriginId,expandedPaneId') || value.presentation.focusedSessionOriginId !== null && origins.indexOf(value.presentation.focusedSessionOriginId) < 0 || value.presentation.expandedPaneId !== null && !value.panes.some(function (p) { return p.kind !== 'session' && p.open && p.instanceId === value.presentation.expandedPaneId; }))) return false;
|
|
46
|
+
return sameTree(sessionTree(value), view.tree) && (value.linkedPair === undefined || validLinkedPair(value.linkedPair, value, { sessionRefs: view.sessions.map(function (s) { return { originId: s.sessionOriginId }; }) }));
|
|
47
|
+
}
|
|
@@ -9,7 +9,7 @@ import { mountWireframeViewport } from './wireframe-viewport.js';
|
|
|
9
9
|
function panel() { return document.getElementById('wireframe-panel'); }
|
|
10
10
|
export function closeWireframe() {
|
|
11
11
|
releaseRightWorkbench('wireframe');
|
|
12
|
-
if (panel()) panel().remove();
|
|
12
|
+
if (panel()) { panel().dispatchEvent(new Event('clay-tool-pane-dispose')); panel().remove(); }
|
|
13
13
|
store.set({ wireframeDraft: null });
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -90,6 +90,7 @@ export function openWireframe(message) {
|
|
|
90
90
|
var draft = { source: wireframeSource(message.source, message.language), title: String(message.title || 'clay-sketch').slice(0, 100),
|
|
91
91
|
id: /^[a-zA-Z0-9_-]{1,80}$/.test(message.id || '') ? message.id : 'wireframe' };
|
|
92
92
|
store.set({ wireframeDraft: draft });
|
|
93
|
+
el.querySelector('[data-save]').disabled = false; el.querySelector('[data-download]').disabled = false;
|
|
93
94
|
el.querySelector('header strong').textContent = draft.title;
|
|
94
95
|
el.querySelector('.wireframe-save-status').textContent = '';
|
|
95
96
|
el.querySelector('form').hidden = true;
|
|
@@ -115,5 +116,15 @@ function draw(draft, el) {
|
|
|
115
116
|
}
|
|
116
117
|
registerRightWorkbench('wireframe', closeWireframe);
|
|
117
118
|
store.subscribe(function (state, previous) {
|
|
118
|
-
if (state.currentSlug !== previous.currentSlug || state.
|
|
119
|
+
if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId) closeWireframe();
|
|
119
120
|
});
|
|
121
|
+
|
|
122
|
+
export function restoreWireframeWorkbench() {
|
|
123
|
+
var draft = store.get('wireframeDraft');
|
|
124
|
+
if (draft) { openWireframe(draft); return; }
|
|
125
|
+
var el = panel() || createPanel();
|
|
126
|
+
if (!el) return;
|
|
127
|
+
claimRightWorkbench('wireframe'); el.classList.remove('hidden');
|
|
128
|
+
el.querySelector('[data-save]').disabled = true; el.querySelector('[data-download]').disabled = true;
|
|
129
|
+
el.querySelector('.wireframe-workbench-canvas').textContent = 'Open a sketch to view it here.';
|
|
130
|
+
}
|
|
@@ -8,7 +8,7 @@ function state() { return store.get('workbenchLayoutPreference'); }
|
|
|
8
8
|
function defaults(previous) {
|
|
9
9
|
var same = previous && previous.accountId === account();
|
|
10
10
|
return { mode: same ? previous.mode : 'overlay', accountId: account(), ready: false, loading: false, saving: false,
|
|
11
|
-
error: '', socket: getWs(), revision: same ? previous.revision : 0, generation: (previous && previous.generation || 0) + 1, checkedAt: 0 };
|
|
11
|
+
layouts: same ? previous.layouts || {} : {}, error: '', socket: getWs(), revision: same ? previous.revision : 0, generation: (previous && previous.generation || 0) + 1, checkedAt: 0 };
|
|
12
12
|
}
|
|
13
13
|
function initialize() {
|
|
14
14
|
if (!state() || state().accountId !== account() || state().socket !== getWs()) store.set({ workbenchLayoutPreference: defaults(state()) });
|
|
@@ -21,7 +21,7 @@ function current(receipt) {
|
|
|
21
21
|
store.get('connected') === receipt.connected && store.get('authIdentityLoaded') === receipt.identity &&
|
|
22
22
|
store.get('projectSplitViewSocketGeneration') === receipt.socketGeneration;
|
|
23
23
|
}
|
|
24
|
-
function request(mode) {
|
|
24
|
+
function request(mode, layout) {
|
|
25
25
|
var before = initialize(), saving = mode !== undefined;
|
|
26
26
|
var receipt = { generation: before.generation, project: store.get('currentSlug'), socket: getWs(),
|
|
27
27
|
connected: store.get('connected'), identity: store.get('authIdentityLoaded'), socketGeneration: store.get('projectSplitViewSocketGeneration') };
|
|
@@ -33,14 +33,15 @@ function request(mode) {
|
|
|
33
33
|
if (!current(receipt)) return false;
|
|
34
34
|
var response = await fetch(endpoint, { method: saving ? 'PUT' : 'GET', credentials: 'same-origin', cache: 'no-store', signal: controller.signal,
|
|
35
35
|
headers: { 'Content-Type': 'application/json', 'X-Clay-Settings': '1', 'X-Clay-Account-Id': before.accountId },
|
|
36
|
-
body: saving ? JSON.stringify({ mode: mode, revision: before.revision }) : undefined });
|
|
36
|
+
body: saving ? JSON.stringify(Object.assign({ mode: mode, revision: before.revision }, layout ? { layout: layout } : {})) : undefined });
|
|
37
37
|
var data = await response.json();
|
|
38
38
|
if (!current(receipt)) return false;
|
|
39
39
|
if (!response.ok) throw new Error(response.status === 409 ? 'Workbench layout changed elsewhere. Reload the preference and try again.' :
|
|
40
40
|
saving ? 'Workbench layout could not be saved. Reload the preference before trying again.' : 'Workbench layout could not be loaded. Try again.');
|
|
41
41
|
if (!data || data.accountId !== before.accountId || !validMode(data.mode) || !Number.isSafeInteger(data.revision) || data.revision < before.revision || data.revision === before.revision && data.mode !== before.mode ||
|
|
42
|
-
saving && (data.mode !== mode || data.revision !== before.revision + (mode === before.mode ? 0 : 1))) throw new Error('Workbench preference response is stale. Reload and try again.');
|
|
43
|
-
|
|
42
|
+
saving && (data.mode !== mode || data.revision !== before.revision + (mode === before.mode && (!layout || JSON.stringify((before.layouts || {})[layout.key]) === JSON.stringify({ base: layout.base, tree: layout.tree })) ? 0 : 1))) throw new Error('Workbench preference response is stale. Reload and try again.');
|
|
43
|
+
if (layout && (!data.layouts || JSON.stringify(data.layouts[layout.key]) !== JSON.stringify({ base: layout.base, tree: layout.tree }))) throw new Error('Window layout response is stale. Reload and try again.');
|
|
44
|
+
store.set({ workbenchLayoutPreference: Object.assign({}, state(), { mode: data.mode, revision: data.revision, layouts: data.layouts || {},
|
|
44
45
|
ready: true, loading: false, saving: false, error: '', checkedAt: Date.now(), receipt: null, promise: null }) });
|
|
45
46
|
return true;
|
|
46
47
|
} catch (error) {
|
|
@@ -74,3 +75,9 @@ store.subscribe(function (next, previous) {
|
|
|
74
75
|
store.set({ workbenchLayoutPreference: defaults(state()) });
|
|
75
76
|
}
|
|
76
77
|
});
|
|
78
|
+
|
|
79
|
+
export function saveWorkbenchWindowLayout(key, tree, base) {
|
|
80
|
+
var value = initialize();
|
|
81
|
+
if (!value.ready || value.loading || value.saving || !store.get('connected') || !store.get('authIdentityLoaded')) return Promise.resolve(false);
|
|
82
|
+
return request(value.mode, { key: key, tree: tree, base: base });
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// One registry for the non-resource workbenches and their existing entrypoints.
|
|
2
|
+
export var WORKBENCH_PANELS = {
|
|
3
|
+
'project-logs': { id: 'project-logs-panel', label: 'Logs', icon: 'notebook-text', open: 'openProjectLogs', load: function () { return import('./project-logs.js'); } },
|
|
4
|
+
you: { id: 'you-panel', label: 'Me', icon: 'user', open: 'openYou', load: function () { return import('./you.js'); } },
|
|
5
|
+
issues: { id: 'issues-panel', label: 'Issues', icon: 'circle-dot', open: 'openIssues', load: function () { return import('./issues.js'); } },
|
|
6
|
+
linear: { id: 'linear-panel', label: 'Linear', icon: 'list-todo', open: 'openLinear', load: function () { return import('./linear-panel.js'); } },
|
|
7
|
+
'notes-browser': { id: 'notes-browser', label: 'Sticky Notes', icon: 'sticky-note', open: 'openNotesBrowser', load: function () { return import('./sticky-notes-browser.js'); } },
|
|
8
|
+
'scheduled-tasks': { id: 'scheduled-tasks-panel', label: 'Scheduled Tasks', icon: 'calendar-clock', open: 'openScheduledTasks', load: function () { return import('./scheduled-tasks.js'); } },
|
|
9
|
+
debates: { id: 'debates-workbench', label: 'Debates', icon: 'messages-square', open: 'openDebatesWorkbench', load: function () { return import('./debates-workbench.js'); } },
|
|
10
|
+
'mcp-skills': { id: 'mcp-skills-workbench', label: 'MCP / Skills', icon: 'blocks', open: 'openMcpSkillsWorkbench', load: function () { return import('./mcp-skills-workbench.js'); } },
|
|
11
|
+
'mate-knowledge': { id: 'mate-knowledge-workbench', label: 'Knowledge', icon: 'book-open', open: 'openKnowledgeWorkbench', load: function () { return import('./mate-knowledge-workbench.js'); } },
|
|
12
|
+
wireframe: { id: 'wireframe-panel', label: 'Sketch', icon: 'panels-top-left', open: 'restoreWireframeWorkbench', load: function () { return import('./wireframe-workbench.js'); } }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function setWorkbenchExpanded(name, expanded) {
|
|
16
|
+
var definition = WORKBENCH_PANELS[name], panel = definition && document.getElementById(definition.id);
|
|
17
|
+
if (!panel || panel.classList.contains('panel-fullscreen') === expanded) return;
|
|
18
|
+
var button = panel.querySelector('header button[aria-label*="fullscreen"], header button[title*="fullscreen"]');
|
|
19
|
+
if (button) button.click();
|
|
20
|
+
else panel.classList.toggle('panel-fullscreen', expanded);
|
|
21
|
+
}
|