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
|
@@ -1,301 +1,15 @@
|
|
|
1
|
-
var fs = require("fs");
|
|
2
|
-
var path = require("path");
|
|
3
|
-
var crypto = require("crypto");
|
|
4
1
|
var layout = require("./split-view-layout");
|
|
5
2
|
var names = require("./project-split-view-names");
|
|
6
3
|
var sessionProvenance = require("./session-provenance");
|
|
7
4
|
var validDocument = require("./project-split-view-document").validDocument;
|
|
5
|
+
var createSplitViewsStore = require("./project-view-store").createSplitViewsStore;
|
|
6
|
+
var workspace = require("./project-view-workspace");
|
|
8
7
|
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
9
|
-
function ownerKey(ownerId) { return ownerId == null ? "default" : "user:" + String(ownerId); }
|
|
10
|
-
function emptyDocument() { return { version: 1, projects: {} }; }
|
|
11
|
-
function emptyOwner(ownerId, linuxUser) { return { ownerId: ownerId == null ? null : String(ownerId), linuxUser: linuxUser || null, revision: 0, views: [] }; }
|
|
12
|
-
function createSplitViewsStore(opts) {
|
|
13
|
-
var slug = opts.slug;
|
|
14
|
-
var sessionsDir = opts.sessionsDir;
|
|
15
|
-
var filePath = opts.filePath || path.join(sessionsDir, "presentation-split-views.json");
|
|
16
|
-
var fsImpl = opts.fs || fs;
|
|
17
|
-
var persist = opts.persist || null;
|
|
18
|
-
var document = emptyDocument();
|
|
19
|
-
var loadError = null;
|
|
20
|
-
try {
|
|
21
|
-
var loaded = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
|
|
22
|
-
if (!validDocument(loaded)) throw new Error("Stored Split view data is invalid");
|
|
23
|
-
document = loaded;
|
|
24
|
-
} catch (error) {
|
|
25
|
-
if (!error || error.code !== "ENOENT") loadError = error;
|
|
26
|
-
}
|
|
27
|
-
function projectRecord(source, create) {
|
|
28
|
-
if (!source.projects[slug] && create) source.projects[slug] = { owners: {} };
|
|
29
|
-
return source.projects[slug] || null;
|
|
30
|
-
}
|
|
31
|
-
function ownerRecord(source, ownerId, linuxUser, create) {
|
|
32
|
-
var project = projectRecord(source, create);
|
|
33
|
-
if (!project) return null;
|
|
34
|
-
var key = ownerKey(ownerId), record = project.owners[key];
|
|
35
|
-
if (!record && create) { record = emptyOwner(ownerId, linuxUser); project.owners[key] = record; }
|
|
36
|
-
if (record && (record.ownerId !== (ownerId == null ? null : String(ownerId)) || record.linuxUser !== (linuxUser || null))) return { identityMismatch: true };
|
|
37
|
-
return record || null;
|
|
38
|
-
}
|
|
39
|
-
function save(next) {
|
|
40
|
-
if (!validDocument(next)) throw new Error("Split view state failed validation");
|
|
41
|
-
var serialized = JSON.stringify(next, null, 2) + "\n";
|
|
42
|
-
if (persist) {
|
|
43
|
-
if (persist(filePath, serialized) === false) throw new Error("Split view persistence returned false");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
fsImpl.mkdirSync(sessionsDir, { recursive: true });
|
|
47
|
-
var tempPath = filePath + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
48
|
-
try {
|
|
49
|
-
fsImpl.writeFileSync(tempPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
50
|
-
if (process.platform !== "win32") try { fsImpl.chmodSync(tempPath, 0o600); } catch (e) {}
|
|
51
|
-
fsImpl.renameSync(tempPath, filePath);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
try { fsImpl.unlinkSync(tempPath); } catch (e) {}
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function state(ownerId, linuxUser) {
|
|
58
|
-
if (loadError) return { ok: false, error: "Stored Split view data could not be read; no changes were made", code: "storage", revision: 0, views: [] };
|
|
59
|
-
var record = ownerRecord(document, ownerId, linuxUser, false);
|
|
60
|
-
if (record && record.identityMismatch) return { ok: false, error: "Split view owner or OS identity changed", code: "identity", revision: 0, views: [] };
|
|
61
|
-
return record ? { ok: true, revision: record.revision, views: clone(record.views) } : { ok: true, revision: 0, views: [] };
|
|
62
|
-
}
|
|
63
|
-
function conflict(actual) { return { ok: false, error: "Split view state changed. Reload and try again.", code: "conflict", revision: actual }; }
|
|
64
|
-
function validateRevision(value) { return Number.isInteger(value) && value >= 0; }
|
|
65
|
-
function prepareRecord(ownerId, linuxUser, expectedRevision) {
|
|
66
|
-
var current = state(ownerId, linuxUser);
|
|
67
|
-
if (!current.ok) return current;
|
|
68
|
-
if (!validateRevision(expectedRevision) || expectedRevision !== current.revision) return conflict(current.revision);
|
|
69
|
-
var next = clone(document), record = ownerRecord(next, ownerId, linuxUser, true);
|
|
70
|
-
return { ok: true, next: next, record: record };
|
|
71
|
-
}
|
|
72
|
-
function commit(ownerId, record, changedView) {
|
|
73
|
-
record.revision++;
|
|
74
|
-
if (Array.isArray(changedView)) changedView.forEach(function (view) { if (view) view.revision = record.revision; });
|
|
75
|
-
else if (changedView) changedView.revision = record.revision;
|
|
76
|
-
try { save(documentForOwner(record, ownerId)); }
|
|
77
|
-
catch (error) { return { ok: false, error: error.message || String(error), code: "storage", revision: state(ownerId, record.linuxUser).revision }; }
|
|
78
|
-
document = documentForOwner(record, ownerId);
|
|
79
|
-
var resultView = Array.isArray(changedView) ? changedView[changedView.length - 1] : changedView;
|
|
80
|
-
return { ok: true, revision: record.revision, view: resultView ? clone(resultView) : null, views: clone(record.views) };
|
|
81
|
-
}
|
|
82
|
-
function documentForOwner(record, ownerId) {
|
|
83
|
-
var next = clone(document), project = projectRecord(next, true);
|
|
84
|
-
project.owners[ownerKey(ownerId)] = clone(record);
|
|
85
|
-
return next;
|
|
86
|
-
}
|
|
87
|
-
function validExpectedView(record, id) {
|
|
88
|
-
if (typeof id !== "string" || !id) return { error: "Split view not found" };
|
|
89
|
-
var index = record.views.findIndex(function (view) { return view.id === id; });
|
|
90
|
-
if (index === -1) return { error: "Split view not found" };
|
|
91
|
-
return { index: index, view: record.views[index] };
|
|
92
|
-
}
|
|
93
|
-
function refsFor(origins, resolvedRefs) {
|
|
94
|
-
if (!Array.isArray(origins) || origins.length < 2 || origins.length > layout.MAX_SESSIONS ||
|
|
95
|
-
origins.some(function (origin) { return !layout.validOrigin(origin); }) || new Set(origins).size !== origins.length) return { error: "A Split view requires two to eight unique session origins" };
|
|
96
|
-
if (!Array.isArray(resolvedRefs) || resolvedRefs.length !== origins.length) return { error: "One or more exact sessions could not be resolved" };
|
|
97
|
-
var refs = [];
|
|
98
|
-
for (var i = 0; i < origins.length; i++) {
|
|
99
|
-
var ref = resolvedRefs.find(function (item) { return item && item.originId === origins[i]; });
|
|
100
|
-
if (!ref) return { error: "One or more exact sessions could not be resolved" };
|
|
101
|
-
refs.push({ originId: ref.originId, ownerId: ref.ownerId == null ? null : String(ref.ownerId), linuxUser: ref.linuxUser || null });
|
|
102
|
-
}
|
|
103
|
-
return { refs: refs };
|
|
104
|
-
}
|
|
105
|
-
function validateComposition(input, refs) {
|
|
106
|
-
var origins = refs.map(function (ref) { return ref.originId; }), tree = layout.validateTree(input.tree, origins), preset = input.layout;
|
|
107
|
-
if (!tree.ok) return { error: tree.error };
|
|
108
|
-
if (!layout.validatePreset(preset)) return { error: "Unknown Split view layout" };
|
|
109
|
-
if (typeof input.activeSessionOriginId !== "string" || origins.indexOf(input.activeSessionOriginId) === -1) return { error: "Active session must belong to the Split view" };
|
|
110
|
-
return { origins: origins };
|
|
111
|
-
}
|
|
112
|
-
function uniqueMembership(views, refs, exceptId, secondExceptId) {
|
|
113
|
-
var used = [];
|
|
114
|
-
views.forEach(function (view) {
|
|
115
|
-
if (view.id === exceptId || view.id === secondExceptId) return;
|
|
116
|
-
view.sessionRefs.forEach(function (ref) { used.push(ref.originId); });
|
|
117
|
-
});
|
|
118
|
-
return refs.every(function (ref) { return used.indexOf(ref.originId) === -1; });
|
|
119
|
-
}
|
|
120
|
-
function create(ownerId, linuxUser, input, resolvedRefs) {
|
|
121
|
-
input = input || {};
|
|
122
|
-
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
123
|
-
if (!prepared.ok) return prepared;
|
|
124
|
-
var name = layout.validateName(input.name), refs = refsFor(input.sessionOrigins, resolvedRefs);
|
|
125
|
-
if (!name.ok) return name;
|
|
126
|
-
if (refs.error) return { ok: false, error: refs.error };
|
|
127
|
-
var composition = validateComposition(input, refs.refs);
|
|
128
|
-
if (composition.error) return { ok: false, error: composition.error };
|
|
129
|
-
if (!uniqueMembership(prepared.record.views, refs.refs)) return { ok: false, error: "A session can belong to only one Split view" };
|
|
130
|
-
var nameMode = input.nameMode === "custom" ? "custom" : "automatic";
|
|
131
|
-
var view = { id: crypto.randomUUID(), name: nameMode === "automatic" ? names.automaticName(input.liveSessions) : name.name, nameMode: nameMode,
|
|
132
|
-
folderId: null, favorite: false, revision: prepared.record.revision + 1, layout: input.layout,
|
|
133
|
-
activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: refs.refs };
|
|
134
|
-
prepared.record.views.push(view);
|
|
135
|
-
return commit(ownerId, prepared.record, view);
|
|
136
|
-
}
|
|
137
|
-
function update(ownerId, linuxUser, input, resolvedRefs) {
|
|
138
|
-
input = input || {};
|
|
139
|
-
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
140
|
-
if (!prepared.ok) return prepared;
|
|
141
|
-
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
142
|
-
if (found.error) return { ok: false, error: found.error };
|
|
143
|
-
if (found.conflict) return conflict(found.revision);
|
|
144
|
-
var refs = refsFor(input.sessionOrigins, resolvedRefs);
|
|
145
|
-
if (refs.error) return { ok: false, error: refs.error };
|
|
146
|
-
var ordered = names.update(found.view, refs.refs, input.liveSessions);
|
|
147
|
-
if (!ordered) return { ok: false, error: "One or more exact sessions could not be resolved" };
|
|
148
|
-
refs.refs = ordered.refs; input.liveSessions = ordered.sessions;
|
|
149
|
-
var composition = validateComposition(input, refs.refs);
|
|
150
|
-
if (composition.error) return { ok: false, error: composition.error };
|
|
151
|
-
if (!uniqueMembership(prepared.record.views, refs.refs, input.id)) return { ok: false, error: "A session can belong to only one Split view" };
|
|
152
|
-
if (names.mode(found.view) === "automatic") found.view.name = names.automaticName(input.liveSessions);
|
|
153
|
-
found.view.layout = input.layout; found.view.activeSessionOriginId = input.activeSessionOriginId;
|
|
154
|
-
found.view.tree = clone(input.tree); found.view.sessionRefs = refs.refs;
|
|
155
|
-
return commit(ownerId, prepared.record, found.view);
|
|
156
|
-
}
|
|
157
|
-
function rename(ownerId, linuxUser, input) {
|
|
158
|
-
input = input || {};
|
|
159
|
-
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
160
|
-
if (!prepared.ok) return prepared;
|
|
161
|
-
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
162
|
-
if (found.error) return { ok: false, error: found.error };
|
|
163
|
-
if (found.conflict) return conflict(found.revision);
|
|
164
|
-
var name = layout.validateName(input.name);
|
|
165
|
-
if (!name.ok) return name;
|
|
166
|
-
found.view.name = name.name;
|
|
167
|
-
found.view.nameMode = "custom";
|
|
168
|
-
return commit(ownerId, prepared.record, found.view);
|
|
169
|
-
}
|
|
170
|
-
function separate(ownerId, linuxUser, input) {
|
|
171
|
-
input = input || {};
|
|
172
|
-
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
173
|
-
if (!prepared.ok) return prepared;
|
|
174
|
-
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
175
|
-
if (found.error) return { ok: false, error: found.error };
|
|
176
|
-
if (found.conflict) return conflict(found.revision);
|
|
177
|
-
var removed = prepared.record.views.splice(found.index, 1)[0];
|
|
178
|
-
return commit(ownerId, prepared.record, null);
|
|
179
|
-
}
|
|
180
|
-
function move(ownerId, linuxUser, input, resolvedSourceRefs, resolvedTargetRefs) {
|
|
181
|
-
input = input || {};
|
|
182
|
-
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
183
|
-
if (!prepared.ok) return prepared;
|
|
184
|
-
if (typeof input.sourceSessionOriginId !== "string" || !layout.validOrigin(input.sourceSessionOriginId)) return { ok: false, error: "A valid source session is required", code: "invalid" };
|
|
185
|
-
var source = validExpectedView(prepared.record, input.sourceViewId);
|
|
186
|
-
if (source.error) return { ok: false, error: "Source Split view not found", code: "not_found", revision: prepared.record.revision };
|
|
187
|
-
var sourceOrigins = source.view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
188
|
-
if (sourceOrigins.indexOf(input.sourceSessionOriginId) === -1) return { ok: false, error: "Source session does not belong to the source Split view", code: "session", revision: prepared.record.revision };
|
|
189
|
-
if (!Array.isArray(resolvedSourceRefs) || resolvedSourceRefs.length !== source.view.sessionRefs.length) return { ok: false, error: "Source sessions could not be resolved", code: "session", revision: prepared.record.revision };
|
|
190
|
-
for (var i = 0; i < source.view.sessionRefs.length; i++) {
|
|
191
|
-
var storedSource = source.view.sessionRefs[i], liveSource = resolvedSourceRefs.find(function (ref) { return ref && ref.originId === storedSource.originId; });
|
|
192
|
-
if (!liveSource || (liveSource.ownerId || null) !== (storedSource.ownerId || null) || (liveSource.linuxUser || null) !== (storedSource.linuxUser || null)) {
|
|
193
|
-
return { ok: false, error: "A source session was replaced or changed identity", code: "session", revision: prepared.record.revision };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
var target = null, targetSessionOriginId = input.targetSessionOriginId;
|
|
197
|
-
var hasTargetView = typeof input.targetViewId === "string" && input.targetViewId.length > 0;
|
|
198
|
-
var hasStandaloneTarget = typeof targetSessionOriginId === "string" && layout.validOrigin(targetSessionOriginId);
|
|
199
|
-
if (hasTargetView === hasStandaloneTarget || (input.targetViewId != null && !hasTargetView)) {
|
|
200
|
-
return { ok: false, error: "Choose exactly one target Split view or standalone session", code: "invalid", revision: prepared.record.revision };
|
|
201
|
-
}
|
|
202
|
-
if (hasTargetView) {
|
|
203
|
-
target = validExpectedView(prepared.record, input.targetViewId);
|
|
204
|
-
if (target.error || target.view.id === source.view.id) return { ok: false, error: "Target Split view is unavailable", code: "not_found", revision: prepared.record.revision };
|
|
205
|
-
}
|
|
206
|
-
if (Array.isArray(input.targetSessionOrigins) && input.targetSessionOrigins.length > layout.MAX_SESSIONS) {
|
|
207
|
-
return { ok: false, error: "A Split view can contain at most eight sessions", code: "limit", revision: prepared.record.revision };
|
|
208
|
-
}
|
|
209
|
-
var targetRefs = refsFor(input.targetSessionOrigins, resolvedTargetRefs);
|
|
210
|
-
if (targetRefs.error) return { ok: false, error: targetRefs.error, code: "session", revision: prepared.record.revision };
|
|
211
|
-
var requestedTargetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
|
|
212
|
-
if (requestedTargetOrigins.indexOf(input.sourceSessionOriginId) === -1) return { ok: false, error: "Target layout must include the moved source session", code: "invalid", revision: prepared.record.revision };
|
|
213
|
-
if (target) {
|
|
214
|
-
var requestedPriorOrigins = target.view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
215
|
-
if (requestedTargetOrigins.length !== requestedPriorOrigins.length + 1 || requestedPriorOrigins.some(function (origin) { return requestedTargetOrigins.indexOf(origin) === -1; })) {
|
|
216
|
-
return { ok: false, error: "Target layout must preserve every existing target session", code: "invalid", revision: prepared.record.revision };
|
|
217
|
-
}
|
|
218
|
-
if (!uniqueMembership(prepared.record.views, targetRefs.refs, target.view.id, source.view.id)) return { ok: false, error: "A target session belongs to another Split view", code: "membership", revision: prepared.record.revision };
|
|
219
|
-
} else if (targetSessionOriginId === input.sourceSessionOriginId || requestedTargetOrigins.length !== 2 || requestedTargetOrigins.indexOf(targetSessionOriginId) === -1) {
|
|
220
|
-
return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
|
|
221
|
-
} else if (!uniqueMembership(prepared.record.views, targetRefs.refs, source.view.id)) {
|
|
222
|
-
return { ok: false, error: "A standalone target session already belongs to a Split view", code: "membership", revision: prepared.record.revision };
|
|
223
|
-
}
|
|
224
|
-
if (target) { var orderedTarget = names.append(target.view, targetRefs.refs, input.liveSessions, input.sourceSessionOriginId);
|
|
225
|
-
if (!orderedTarget) return { ok: false, error: "Target layout must preserve existing sessions and include the moved source exactly once", code: "invalid", revision: prepared.record.revision };
|
|
226
|
-
targetRefs.refs = orderedTarget.refs; input.liveSessions = orderedTarget.sessions; }
|
|
227
|
-
else { var orderedStandalone = names.targetFirst(targetRefs.refs, input.liveSessions, targetSessionOriginId, input.sourceSessionOriginId);
|
|
228
|
-
if (!orderedStandalone) return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
|
|
229
|
-
targetRefs.refs = orderedStandalone.refs; input.liveSessions = orderedStandalone.sessions; }
|
|
230
|
-
var targetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
|
|
231
|
-
if (!layout.validatePreset(input.layout) || !layout.validateTree(input.tree, targetOrigins).ok || targetOrigins.length > layout.MAX_SESSIONS) {
|
|
232
|
-
return { ok: false, error: "Target layout is invalid or exceeds the eight-session limit", code: targetOrigins.length > layout.MAX_SESSIONS ? "limit" : "invalid", revision: prepared.record.revision };
|
|
233
|
-
}
|
|
234
|
-
if (typeof input.activeSessionOriginId !== "string" || targetOrigins.indexOf(input.activeSessionOriginId) === -1) {
|
|
235
|
-
return { ok: false, error: "Active session must belong to the target Split view", code: "invalid", revision: prepared.record.revision };
|
|
236
|
-
}
|
|
237
|
-
if (!target) {
|
|
238
|
-
var name = layout.validateName(input.name);
|
|
239
|
-
if (!name.ok) return { ok: false, error: name.error, code: "invalid", revision: prepared.record.revision };
|
|
240
|
-
}
|
|
241
|
-
var nextSourceOrigins = sourceOrigins.filter(function (origin) { return origin !== input.sourceSessionOriginId; });
|
|
242
|
-
source.view.tree = layout.removeLeaf(source.view.tree, input.sourceSessionOriginId);
|
|
243
|
-
source.view.sessionRefs = source.view.sessionRefs.filter(function (ref) { return ref.originId !== input.sourceSessionOriginId; });
|
|
244
|
-
if (source.view.activeSessionOriginId === input.sourceSessionOriginId && nextSourceOrigins.length) source.view.activeSessionOriginId = nextSourceOrigins[0];
|
|
245
|
-
if (nextSourceOrigins.length < 2) prepared.record.views.splice(source.index, 1);
|
|
246
|
-
if (nextSourceOrigins.length >= 2 && names.mode(source.view) === "automatic") source.view.name = names.automaticName(input.sourceLiveSessions.filter(function (session) { return session.sessionOriginId !== input.sourceSessionOriginId; }));
|
|
247
|
-
if (target) {
|
|
248
|
-
target.view.tree = clone(input.tree); target.view.layout = input.layout;
|
|
249
|
-
target.view.activeSessionOriginId = input.activeSessionOriginId; target.view.sessionRefs = targetRefs.refs;
|
|
250
|
-
if (names.mode(target.view) === "automatic") target.view.name = names.automaticName(input.liveSessions);
|
|
251
|
-
} else {
|
|
252
|
-
target = { index: -1, view: { id: crypto.randomUUID(), folderId: null, favorite: false, name: names.automaticName(input.liveSessions), nameMode: "automatic", revision: prepared.record.revision + 1,
|
|
253
|
-
layout: input.layout, activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: targetRefs.refs } };
|
|
254
|
-
prepared.record.views.push(target.view);
|
|
255
|
-
}
|
|
256
|
-
var changed = [target.view];
|
|
257
|
-
if (nextSourceOrigins.length >= 2) changed.unshift(source.view);
|
|
258
|
-
return commit(ownerId, prepared.record, changed);
|
|
259
|
-
}
|
|
260
|
-
function removeOrigin(originId) {
|
|
261
|
-
var project = projectRecord(document, false), changedOwners = [], failures = [];
|
|
262
|
-
if (!project) return { owners: changedOwners, failures: failures };
|
|
263
|
-
var keys = Object.keys(project.owners);
|
|
264
|
-
for (var i = 0; i < keys.length; i++) {
|
|
265
|
-
var source = project.owners[keys[i]], next = clone(source), changed = false, retained = [];
|
|
266
|
-
for (var j = 0; j < next.views.length; j++) {
|
|
267
|
-
var view = next.views[j], member = view.sessionRefs.some(function (ref) { return ref.originId === originId; });
|
|
268
|
-
if (!member) { retained.push(view); continue; }
|
|
269
|
-
changed = true;
|
|
270
|
-
var tree = layout.removeLeaf(view.tree, originId), origins = view.sessionRefs.filter(function (ref) { return ref.originId !== originId; });
|
|
271
|
-
if (origins.length < 2) continue;
|
|
272
|
-
view.tree = tree;
|
|
273
|
-
view.sessionRefs = origins;
|
|
274
|
-
if (view.activeSessionOriginId === originId) view.activeSessionOriginId = origins[0].originId;
|
|
275
|
-
retained.push(view);
|
|
276
|
-
}
|
|
277
|
-
if (!changed) continue;
|
|
278
|
-
next.views = retained;
|
|
279
|
-
next.revision++;
|
|
280
|
-
next.views.forEach(function (view) { view.revision = next.revision; });
|
|
281
|
-
try {
|
|
282
|
-
var replacement = documentForOwner(next, source.ownerId);
|
|
283
|
-
save(replacement);
|
|
284
|
-
document = replacement;
|
|
285
|
-
changedOwners.push(source.ownerId);
|
|
286
|
-
} catch (error) { failures.push({ ownerId: source.ownerId, error: error.message || String(error) }); }
|
|
287
|
-
}
|
|
288
|
-
return { owners: changedOwners, failures: failures };
|
|
289
|
-
}
|
|
290
|
-
var organization = require('./project-split-view-organization').attachOrganization({ prepare: prepareRecord, commit: commit, state: state });
|
|
291
|
-
return { organize: organization.organize, reconcileFolders: organization.reconcileFolders, state: state, create: create, update: update, rename: rename, separate: separate, move: move, removeOrigin: removeOrigin,
|
|
292
|
-
get filePath() { return filePath; }, get loadError() { return loadError; } };
|
|
293
|
-
}
|
|
294
8
|
function attachProjectSplitViews(ctx) {
|
|
295
9
|
var store = createSplitViewsStore({ sessionsDir: ctx.sm.sessionsDir, slug: ctx.slug, persist: ctx.persist });
|
|
296
10
|
var users = ctx.usersModule;
|
|
297
11
|
function actor(ws) {
|
|
298
|
-
if (ctx.isMate || !ws || !ctx.clients.has(ws) || ws._clayPaneActivation || !ctx.canAccessProject(ws)) return null;
|
|
12
|
+
if (ctx.isMate || !ws || ws.readyState !== 1 || !ctx.clients.has(ws) || ws._clayPaneActivation || !ctx.canAccessProject(ws)) return null;
|
|
299
13
|
if (users.isMultiUser()) {
|
|
300
14
|
var user = ws._clayUser && users.findUserById(ws._clayUser.id);
|
|
301
15
|
if (!user || !ctx.canAccessProject(ws)) return null;
|
|
@@ -320,7 +34,7 @@ function attachProjectSplitViews(ctx) {
|
|
|
320
34
|
return { session: found, ref: { originId: found.sessionOriginId, ownerId: found.ownerId || null, linuxUser: linuxUser || null } };
|
|
321
35
|
}
|
|
322
36
|
function resolvedRefs(ws, origins) {
|
|
323
|
-
if (!Array.isArray(origins)
|
|
37
|
+
if (!Array.isArray(origins)) return { error: "View session origins must be an array", code: "invalid" };
|
|
324
38
|
if (origins.length > layout.MAX_SESSIONS) return { error: "A Split view can contain at most eight sessions", code: "limit" };
|
|
325
39
|
if (new Set(origins).size !== origins.length || origins.some(function (origin) { return !layout.validOrigin(origin); })) return { error: "Split view session origins must be unique valid values", code: "invalid" };
|
|
326
40
|
var refs = [], sessions = [];
|
|
@@ -353,24 +67,31 @@ function attachProjectSplitViews(ctx) {
|
|
|
353
67
|
(resolved.ref.linuxUser || null) !== (stored.linuxUser || null)) return null;
|
|
354
68
|
refs.push({ id: resolved.session.localId, sessionOriginId: resolved.session.sessionOriginId, title: resolved.session.title || "" });
|
|
355
69
|
}
|
|
356
|
-
var projected = names.project(view, refs); projected.tree = clone(view.tree); return projected;
|
|
70
|
+
var projected = names.project(view, refs); projected.tree = clone(view.tree); projected.workspace = workspace.normalized(view).workspace; return projected;
|
|
357
71
|
}
|
|
358
72
|
function folderIds(ownerId) {
|
|
359
73
|
var accountId = users.isMultiUser() ? ownerId : 'default';
|
|
360
74
|
return typeof users.getSessionFolders === 'function' ? users.getSessionFolders(accountId, ctx.slug).folders.map(function (folder) { return folder.id; }) : [];
|
|
361
75
|
}
|
|
76
|
+
function destinationFolder(ownerId, view, origin) {
|
|
77
|
+
var state = typeof users.getSessionFolders === 'function' ? users.getSessionFolders(users.isMultiUser() ? ownerId : 'default', ctx.slug) : null;
|
|
78
|
+
if (!state) return null;
|
|
79
|
+
// Singleton Views still appear as session rows and use session placement.
|
|
80
|
+
var id = view && view.sessionRefs.length !== 1 ? view.folderId : (state.assignments || {})[view ? view.sessionRefs[0].originId : origin];
|
|
81
|
+
return state.folders.some(function (folder) { return folder.id === id; }) ? id : null;
|
|
82
|
+
}
|
|
362
83
|
function stateFor(ws) {
|
|
363
84
|
var current = actor(ws);
|
|
364
85
|
if (!current) return null;
|
|
365
86
|
var state = store.reconcileFolders(current.id, current.linuxUser, folderIds(current.id));
|
|
366
87
|
if (!state.ok) return { owner: current, revision: state.revision, views: [], error: state.error };
|
|
367
88
|
var views = state.views.map(function (view) { return visibleView(ws, view); }).filter(Boolean);
|
|
368
|
-
return { owner: current, revision: state.revision, views: views };
|
|
89
|
+
return { owner: current, revision: state.revision, activeViewId: state.activeViewId || null, views: views };
|
|
369
90
|
}
|
|
370
91
|
function sendState(ws) {
|
|
371
92
|
var state = stateFor(ws);
|
|
372
93
|
if (!state) return false;
|
|
373
|
-
ctx.sendTo(ws, { type: "project_split_views", projectSlug: ctx.slug, revision: state.revision, views: state.views, error: state.error || null });
|
|
94
|
+
ctx.sendTo(ws, { type: "project_split_views", projectSlug: ctx.slug, revision: state.revision, activeViewId: state.activeViewId || null, views: state.views, error: state.error || null });
|
|
374
95
|
return true;
|
|
375
96
|
}
|
|
376
97
|
function broadcastOwner(ownerId) {
|
|
@@ -405,22 +126,29 @@ function attachProjectSplitViews(ctx) {
|
|
|
405
126
|
function execute(action, input) {
|
|
406
127
|
var current = currentActor(), result;
|
|
407
128
|
if (!current) return { ok: false, error: "Split view access is no longer permitted", code: "forbidden" };
|
|
408
|
-
input = input || {};
|
|
129
|
+
input = Object.assign({}, input || {});
|
|
130
|
+
var identity = require("./project-view-input").check(input, ctx.slug, current.id, input.workspace !== undefined);
|
|
131
|
+
if (!identity.ok) return identity;
|
|
409
132
|
var currentState = store.state(current.id, current.linuxUser);
|
|
410
133
|
if (!currentState.ok) return currentState;
|
|
411
134
|
if (!Number.isInteger(input.expectedRevision) || input.expectedRevision !== currentState.revision) {
|
|
412
135
|
return { ok: false, error: "Split view state changed. Reload and try again.", code: "conflict", revision: currentState.revision };
|
|
413
136
|
}
|
|
137
|
+
var targetId = action === "move" ? input.sourceViewId : input.id;
|
|
138
|
+
var target = currentState.views.find(function (view) { return view.id === targetId; });
|
|
139
|
+
if (target && input.expectedViewRevision !== undefined && target.revision !== input.expectedViewRevision) return { ok: false, code: "conflict", error: "View revision changed. Reload and try again." };
|
|
414
140
|
if (action === "create" || action === "update") {
|
|
415
141
|
var refs = resolvedRefs(ws, input.sessionOrigins);
|
|
416
142
|
if (!refs.refs) return refs;
|
|
143
|
+
if (input.workspace && !require('./project-view-pair').authorized(ctx, ws, input.workspace.linkedPair)) return { ok: false, code: 'pair', error: 'The exact linked Worker pair is unavailable.' };
|
|
417
144
|
input.liveSessions = refs.sessions;
|
|
418
145
|
if (action === "update") {
|
|
419
146
|
var exact = verifyStoredView(ws, current, input.id);
|
|
420
|
-
|
|
147
|
+
var inheritedFolder = target && target.sessionRefs.length === 1 && input.sessionOrigins.length > 1 ? destinationFolder(current.id, target) : undefined;
|
|
148
|
+
return exact.ok ? store.update(current.id, current.linuxUser, input, refs.refs, inheritedFolder) : exact;
|
|
421
149
|
}
|
|
422
150
|
input.nameMode = "automatic";
|
|
423
|
-
return store.create(current.id, current.linuxUser, input, refs.refs);
|
|
151
|
+
return store.create(current.id, current.linuxUser, input, refs.refs, destinationFolder(current.id, null, input.activeSessionOriginId));
|
|
424
152
|
}
|
|
425
153
|
if (action === "move") {
|
|
426
154
|
var sourceState = store.state(current.id, current.linuxUser);
|
|
@@ -437,8 +165,10 @@ function attachProjectSplitViews(ctx) {
|
|
|
437
165
|
}
|
|
438
166
|
var targetResolved = resolvedRefs(ws, input.targetSessionOrigins);
|
|
439
167
|
if (!targetResolved.refs) return targetResolved;
|
|
168
|
+
if (input.targetWorkspace && !require('./project-view-pair').authorized(ctx, ws, input.targetWorkspace.linkedPair)) return { ok: false, code: 'pair', error: 'The exact linked Worker pair is unavailable.' };
|
|
440
169
|
input.liveSessions = targetResolved.sessions;
|
|
441
|
-
|
|
170
|
+
var destination = sourceState.views.find(function (view) { return view.id === input.targetViewId; });
|
|
171
|
+
return store.move(current.id, current.linuxUser, input, sourceResolved.refs, targetResolved.refs, destinationFolder(current.id, destination, input.targetSessionOriginId));
|
|
442
172
|
}
|
|
443
173
|
if (action === 'organize' && (input.projectSlug !== ctx.slug || input.folderId !== null && folderIds(current.id).indexOf(input.folderId) < 0)) return { ok: false, code: 'folder', error: 'That project folder is no longer available.' };
|
|
444
174
|
var exactView = verifyStoredView(ws, current, input.id);
|
|
@@ -270,12 +270,13 @@ function attachUserMessage(ctx) {
|
|
|
270
270
|
return true;
|
|
271
271
|
}
|
|
272
272
|
tm.attach(t.id, ws);
|
|
273
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
273
|
+
send({ type: "term_list", terminals: tm.list(true) });
|
|
274
274
|
sendTo(ws, { type: "term_created", id: t.id });
|
|
275
275
|
return true;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
if (msg.type === "term_attach") {
|
|
279
|
+
if (require("./project-view-terminal-binding").attachSaved(ctx, ws, msg)) return true;
|
|
279
280
|
if (msg.id) tm.attach(msg.id, ws);
|
|
280
281
|
return true;
|
|
281
282
|
}
|
|
@@ -300,7 +301,7 @@ function attachUserMessage(ctx) {
|
|
|
300
301
|
if (msg.type === "term_close") {
|
|
301
302
|
if (msg.id) {
|
|
302
303
|
tm.close(msg.id);
|
|
303
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
304
|
+
send({ type: "term_list", terminals: tm.list(true) });
|
|
304
305
|
// Remove closed terminal from context sources
|
|
305
306
|
var _termSessionId = ws._clayActiveSession || null;
|
|
306
307
|
var saved = loadContextSources(slug, _termSessionId);
|
|
@@ -317,7 +318,7 @@ function attachUserMessage(ctx) {
|
|
|
317
318
|
if (msg.type === "term_rename") {
|
|
318
319
|
if (msg.id && msg.title) {
|
|
319
320
|
tm.rename(msg.id, msg.title);
|
|
320
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
321
|
+
send({ type: "term_list", terminals: tm.list(true) });
|
|
321
322
|
}
|
|
322
323
|
return true;
|
|
323
324
|
}
|
|
@@ -163,7 +163,7 @@ function attachVendorLogin(ctx) {
|
|
|
163
163
|
if (!flow || typeof flow.terminalId !== "number") return;
|
|
164
164
|
flow.closing = true;
|
|
165
165
|
try { tm.close(flow.terminalId); } catch (e) {}
|
|
166
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
166
|
+
send({ type: "term_list", terminals: tm.list(true) });
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
// Restart the vendor adapter so the next query spawns a process that reads
|
|
@@ -299,7 +299,7 @@ function attachVendorLogin(ctx) {
|
|
|
299
299
|
|
|
300
300
|
flow.terminalId = terminal.id;
|
|
301
301
|
if (!flow.browser) tm.attach(terminal.id, ws);
|
|
302
|
-
send({ type: "term_list", terminals: tm.list() });
|
|
302
|
+
send({ type: "term_list", terminals: tm.list(true) });
|
|
303
303
|
return flow;
|
|
304
304
|
}
|
|
305
305
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function check(input, slug, ownerId, extended, view) {
|
|
2
|
+
if (input.projectSlug !== undefined && input.projectSlug !== slug || input.accountId !== undefined && input.accountId !== ownerId ||
|
|
3
|
+
extended && (input.projectSlug !== slug || input.accountId !== ownerId)) {
|
|
4
|
+
return { ok: false, code: 'identity', error: 'View account or project identity does not match' };
|
|
5
|
+
}
|
|
6
|
+
if (view && (extended || input.expectedViewRevision !== undefined) && input.expectedViewRevision !== view.revision) {
|
|
7
|
+
return { ok: false, code: 'conflict', error: 'View revision changed. Reload and try again.' };
|
|
8
|
+
}
|
|
9
|
+
return { ok: true };
|
|
10
|
+
}
|
|
11
|
+
module.exports = { check: check };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Pair presentation is UI state, never Worker membership or execution authority.
|
|
2
|
+
function valid(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
|
+
}
|
|
13
|
+
function authorized(ctx, ws, pair) {
|
|
14
|
+
if (!pair) return true;
|
|
15
|
+
if (typeof pair !== 'object' || !Array.isArray(pair.workerOrigins)) return false;
|
|
16
|
+
var driver;
|
|
17
|
+
ctx.sm.sessions.forEach(function (s) { if (s.sessionOriginId === pair.driverOriginId) driver = s; });
|
|
18
|
+
var group = driver && ctx.getViewPairGroup && ctx.getViewPairGroup(driver.localId);
|
|
19
|
+
// The registry's pair fields support legacy and current Worker lists.
|
|
20
|
+
var p = group && group.pair, ids = p && (p.workerIds || [p.workerId]);
|
|
21
|
+
return !!(driver && (driver.ownerId || null) === (ws._clayUser && ws._clayUser.id || null) && group && group.id === pair.groupId && p && p.driverId === driver.localId && ids.length === pair.workerOrigins.length && ids.every(function (id, i) {
|
|
22
|
+
var s = ctx.sm.sessions.get(id);
|
|
23
|
+
return s && (s.ownerId || null) === (driver.ownerId || null) && require('./session-provenance').isWorker(s) && s.sessionProvenance.parentSessionOriginId === driver.sessionOriginId && s.sessionOriginId === pair.workerOrigins[i] && ctx.canAccessSession(ws,s);
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
module.exports = { valid: valid, authorized: authorized };
|