clay-server 5.3.0-beta.5 → 5.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/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-file-history.js +11 -9
- package/lib/project-file-watch.js +26 -7
- package/lib/project-filesystem.js +5 -4
- package/lib/project-shared-browser.js +7 -3
- package/lib/project-split-view-document.js +16 -2
- package/lib/project-split-views.js +15 -294
- package/lib/project-user-message.js +4 -3
- package/lib/project-vendor-login.js +2 -2
- package/lib/project-view-input.js +11 -0
- package/lib/project-view-pair.js +26 -0
- package/lib/project-view-store.js +346 -0
- package/lib/project-view-terminal-binding.js +16 -0
- package/lib/project-view-workspace.js +126 -0
- package/lib/project.js +1 -0
- package/lib/public/app.js +2 -0
- package/lib/public/css/issues.css +3 -0
- package/lib/public/css/pane.css +5 -1
- package/lib/public/css/right-workbench.css +0 -1
- package/lib/public/css/scheduled-tasks.css +7 -0
- package/lib/public/css/split-view-layout.css +37 -3
- package/lib/public/css/tool-pane.css +127 -0
- package/lib/public/index.html +1 -1
- 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/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 +2 -0
- 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/plantuml-preview.js +12 -12
- package/lib/public/modules/right-workbench.js +34 -15
- package/lib/public/modules/scheduled-tasks.js +7 -6
- package/lib/public/modules/shared-browser.js +69 -9
- package/lib/public/modules/split-pane-renderer.js +1 -0
- package/lib/public/modules/split-view-drag-preview.js +90 -0
- package/lib/public/modules/split-view-layout-client.js +1 -1
- package/lib/public/modules/split-view-presentation-drag.js +36 -13
- package/lib/public/modules/split-view-presentation-drop.js +21 -18
- package/lib/public/modules/split-view-presentation-renderer.js +15 -10
- package/lib/public/modules/split-view-presentations.js +25 -33
- package/lib/public/modules/split-view.js +9 -2
- package/lib/public/modules/sticky-notes-browser.js +2 -2
- package/lib/public/modules/terminal.js +46 -19
- package/lib/public/modules/theme.js +1 -6
- package/lib/public/modules/tool-pane-chrome.js +82 -0
- package/lib/public/modules/tool-pane-drag.js +134 -0
- package/lib/public/modules/tool-pane-layout.js +95 -0
- package/lib/public/modules/tool-pane-legacy.js +60 -0
- package/lib/public/modules/tool-pane-tree.js +51 -0
- package/lib/public/modules/view-legacy-coordinates.js +16 -0
- package/lib/public/modules/view-mutations.js +24 -0
- package/lib/public/modules/view-pair-presentation.js +74 -0
- package/lib/public/modules/view-pair-validation.js +12 -0
- package/lib/public/modules/view-presentation-state.js +37 -0
- package/lib/public/modules/view-workspace-controller.js +210 -0
- package/lib/public/modules/view-workspace-state.js +103 -0
- package/lib/public/modules/view-workspace-validation.js +47 -0
- package/lib/public/modules/wireframe-workbench.js +11 -0
- package/lib/public/modules/workbench-layout-preference.js +12 -5
- package/lib/public/modules/workbench-panels.js +21 -0
- package/lib/public/pane-assets/{app-6H5S2Y3Z.js → app-7UZ727NA.js} +1 -1
- package/lib/public/pane-assets/chunk-2AIWCI6Y.js +1 -0
- package/lib/public/pane-assets/chunk-6MIHIQ2W.js +1 -0
- package/lib/public/pane-assets/chunk-6NYCNWVF.js +1 -0
- package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
- package/lib/public/pane-assets/chunk-6UJ2ALOB.js +1 -0
- package/lib/public/pane-assets/chunk-6XK2OATZ.js +1 -0
- package/lib/public/pane-assets/chunk-BKIDR2ME.js +1 -0
- package/lib/public/pane-assets/chunk-BMHTBRMO.js +1 -0
- package/lib/public/pane-assets/chunk-GEFCFZEN.js +1 -0
- package/lib/public/pane-assets/chunk-GN2NQEVC.js +2 -0
- package/lib/public/pane-assets/chunk-IMLMBPT4.js +166 -0
- package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
- package/lib/public/pane-assets/chunk-K5S6AILM.js +1 -0
- package/lib/public/pane-assets/chunk-LDIZLKB2.js +1 -0
- package/lib/public/pane-assets/chunk-OWNNLZ36.js +1 -0
- package/lib/public/pane-assets/chunk-P2C3LOWL.js +3 -0
- package/lib/public/pane-assets/chunk-PV4PCH2V.js +2 -0
- package/lib/public/pane-assets/chunk-R4LMPTKU.js +1 -0
- package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
- package/lib/public/pane-assets/chunk-TMGSDFJI.js +1 -0
- package/lib/public/pane-assets/chunk-VKYBAVWT.js +1 -0
- package/lib/public/pane-assets/chunk-VNAV23CM.js +1 -0
- package/lib/public/pane-assets/chunk-VWTHIQCE.js +1 -0
- package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
- package/lib/public/pane-assets/chunk-WM3K5AFH.js +1 -0
- package/lib/public/pane-assets/chunk-XOB3O327.js +1 -0
- package/lib/public/pane-assets/chunk-XOOUCFYT.js +1 -0
- package/lib/public/pane-assets/manifest.json +426 -381
- package/lib/public/pane-assets/style-ZM7OOU3M.css +1 -0
- package/lib/public/style.css +1 -0
- package/lib/server-workbench-layout-settings.js +3 -3
- package/lib/split-view-layout.js +11 -7
- package/lib/users-workbench-layout-preferences.js +14 -4
- package/lib/workbench-window-layout.js +30 -0
- package/package.json +1 -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,346 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
4
|
+
var layout = require("./split-view-layout");
|
|
5
|
+
var workspace = require("./project-view-workspace");
|
|
6
|
+
var checkInput = require("./project-view-input").check;
|
|
7
|
+
var names = require("./project-split-view-names");
|
|
8
|
+
var validDocument = require("./project-split-view-document").validDocument;
|
|
9
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
10
|
+
function ownerKey(ownerId) { return ownerId == null ? "default" : "user:" + String(ownerId); }
|
|
11
|
+
function emptyDocument() { return { version: 1, projects: {} }; }
|
|
12
|
+
function emptyOwner(ownerId, linuxUser) { return { ownerId: ownerId == null ? null : String(ownerId), linuxUser: linuxUser || null, revision: 0, views: [] }; }
|
|
13
|
+
function createSplitViewsStore(opts) {
|
|
14
|
+
var slug = opts.slug;
|
|
15
|
+
var sessionsDir = opts.sessionsDir;
|
|
16
|
+
var filePath = opts.filePath || path.join(sessionsDir, "presentation-split-views.json");
|
|
17
|
+
var fsImpl = opts.fs || fs;
|
|
18
|
+
var persist = opts.persist || null;
|
|
19
|
+
var document = emptyDocument();
|
|
20
|
+
var loadError = null;
|
|
21
|
+
try {
|
|
22
|
+
var loaded = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
|
|
23
|
+
if (!validDocument(loaded)) throw new Error("Stored Split view data is invalid");
|
|
24
|
+
document = loaded;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!error || error.code !== "ENOENT") loadError = error;
|
|
27
|
+
}
|
|
28
|
+
function projectRecord(source, create) {
|
|
29
|
+
if (!source.projects[slug] && create) source.projects[slug] = { owners: {} };
|
|
30
|
+
return source.projects[slug] || null;
|
|
31
|
+
}
|
|
32
|
+
function ownerRecord(source, ownerId, linuxUser, create) {
|
|
33
|
+
var project = projectRecord(source, create);
|
|
34
|
+
if (!project) return null;
|
|
35
|
+
var key = ownerKey(ownerId), record = project.owners[key];
|
|
36
|
+
if (!record && create) { record = emptyOwner(ownerId, linuxUser); project.owners[key] = record; }
|
|
37
|
+
if (record && (record.ownerId !== (ownerId == null ? null : String(ownerId)) || record.linuxUser !== (linuxUser || null))) return { identityMismatch: true };
|
|
38
|
+
return record || null;
|
|
39
|
+
}
|
|
40
|
+
function save(next) {
|
|
41
|
+
if (!validDocument(next)) throw new Error("Split view state failed validation");
|
|
42
|
+
var serialized = JSON.stringify(next, null, 2) + "\n";
|
|
43
|
+
if (persist) {
|
|
44
|
+
if (persist(filePath, serialized) === false) throw new Error("Split view persistence returned false");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
fsImpl.mkdirSync(sessionsDir, { recursive: true });
|
|
48
|
+
var tempPath = filePath + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
49
|
+
try {
|
|
50
|
+
fsImpl.writeFileSync(tempPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
51
|
+
if (process.platform !== "win32") try { fsImpl.chmodSync(tempPath, 0o600); } catch (e) {}
|
|
52
|
+
fsImpl.renameSync(tempPath, filePath);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
try { fsImpl.unlinkSync(tempPath); } catch (e) {}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function state(ownerId, linuxUser) {
|
|
59
|
+
if (loadError) return { ok: false, error: "Stored Split view data could not be read; no changes were made", code: "storage", revision: 0, views: [] };
|
|
60
|
+
var record = ownerRecord(document, ownerId, linuxUser, false);
|
|
61
|
+
if (record && record.identityMismatch) return { ok: false, error: "Split view owner or OS identity changed", code: "identity", revision: 0, views: [] };
|
|
62
|
+
return record ? { ok: true, revision: record.revision, activeViewId: record.activeViewId || null, views: record.views.map(workspace.normalized) } : { ok: true, revision: 0, views: [] };
|
|
63
|
+
}
|
|
64
|
+
function conflict(actual) { return { ok: false, error: "Split view state changed. Reload and try again.", code: "conflict", revision: actual }; }
|
|
65
|
+
function validateRevision(value) { return Number.isSafeInteger(value) && value >= 0; }
|
|
66
|
+
function prepareRecord(ownerId, linuxUser, expectedRevision) {
|
|
67
|
+
var current = state(ownerId, linuxUser);
|
|
68
|
+
if (!current.ok) return current;
|
|
69
|
+
if (!validateRevision(expectedRevision) || expectedRevision !== current.revision) return conflict(current.revision);
|
|
70
|
+
var next = clone(document), record = ownerRecord(next, ownerId, linuxUser, true);
|
|
71
|
+
return { ok: true, next: next, record: record };
|
|
72
|
+
}
|
|
73
|
+
function commit(ownerId, record, changedView) {
|
|
74
|
+
record.revision++;
|
|
75
|
+
if (Array.isArray(changedView)) changedView.forEach(function (view) { if (view) view.revision = record.revision; });
|
|
76
|
+
else if (changedView) changedView.revision = record.revision;
|
|
77
|
+
try { save(documentForOwner(record, ownerId)); }
|
|
78
|
+
catch (error) { return { ok: false, error: error.message || String(error), code: "storage", revision: state(ownerId, record.linuxUser).revision }; }
|
|
79
|
+
document = documentForOwner(record, ownerId);
|
|
80
|
+
var resultView = Array.isArray(changedView) ? changedView[changedView.length - 1] : changedView;
|
|
81
|
+
return { ok: true, revision: record.revision, view: resultView ? workspace.normalized(resultView) : null, views: record.views.map(workspace.normalized) };
|
|
82
|
+
}
|
|
83
|
+
function documentForOwner(record, ownerId) {
|
|
84
|
+
var next = clone(document), project = projectRecord(next, true);
|
|
85
|
+
project.owners[ownerKey(ownerId)] = clone(record);
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
function validExpectedView(record, id) {
|
|
89
|
+
if (typeof id !== "string" || !id) return { error: "Split view not found" };
|
|
90
|
+
var index = record.views.findIndex(function (view) { return view.id === id; });
|
|
91
|
+
if (index === -1) return { error: "Split view not found" };
|
|
92
|
+
return { index: index, view: record.views[index] };
|
|
93
|
+
}
|
|
94
|
+
function refsFor(origins, resolvedRefs) {
|
|
95
|
+
if (!Array.isArray(origins) || origins.length > layout.MAX_SESSIONS ||
|
|
96
|
+
origins.some(function (origin) { return !layout.validOrigin(origin); }) || new Set(origins).size !== origins.length) return { error: "A View supports zero to eight unique session origins" };
|
|
97
|
+
if (!Array.isArray(resolvedRefs) || resolvedRefs.length !== origins.length) return { error: "One or more exact sessions could not be resolved" };
|
|
98
|
+
var refs = [];
|
|
99
|
+
for (var i = 0; i < origins.length; i++) {
|
|
100
|
+
var ref = resolvedRefs.find(function (item) { return item && item.originId === origins[i]; });
|
|
101
|
+
if (!ref) return { error: "One or more exact sessions could not be resolved" };
|
|
102
|
+
refs.push({ originId: ref.originId, ownerId: ref.ownerId == null ? null : String(ref.ownerId), linuxUser: ref.linuxUser || null });
|
|
103
|
+
}
|
|
104
|
+
return { refs: refs };
|
|
105
|
+
}
|
|
106
|
+
function validateComposition(input, refs) {
|
|
107
|
+
var origins = refs.map(function (ref) { return ref.originId; }), tree = layout.validateTree(input.tree, origins), preset = input.layout;
|
|
108
|
+
if (!tree.ok) return { error: tree.error };
|
|
109
|
+
if (!layout.validatePreset(preset)) return { error: "Unknown Split view layout" };
|
|
110
|
+
if (origins.length ? typeof input.activeSessionOriginId !== "string" || origins.indexOf(input.activeSessionOriginId) === -1 : input.activeSessionOriginId !== null) return { error: "Active session must belong to the Split view" };
|
|
111
|
+
if (input.workspace !== undefined) {
|
|
112
|
+
var checked = workspace.validate(input.workspace, { tree: input.tree, sessionRefs: refs });
|
|
113
|
+
if (!checked.ok) return { error: checked.error };
|
|
114
|
+
}
|
|
115
|
+
return { origins: origins };
|
|
116
|
+
}
|
|
117
|
+
function uniqueMembership(views, refs, exceptId, secondExceptId) {
|
|
118
|
+
var used = [];
|
|
119
|
+
views.forEach(function (view) {
|
|
120
|
+
if (view.id === exceptId || view.id === secondExceptId) return;
|
|
121
|
+
view.sessionRefs.forEach(function (ref) { used.push(ref.originId); });
|
|
122
|
+
});
|
|
123
|
+
return refs.every(function (ref) { return used.indexOf(ref.originId) === -1; });
|
|
124
|
+
}
|
|
125
|
+
function create(ownerId, linuxUser, input, resolvedRefs) {
|
|
126
|
+
input = input || {};
|
|
127
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
128
|
+
if (!prepared.ok) return prepared;
|
|
129
|
+
var identity = checkInput(input, slug, ownerId, input.workspace !== undefined || Array.isArray(input.sessionOrigins) && input.sessionOrigins.length < 2);
|
|
130
|
+
if (!identity.ok) return identity;
|
|
131
|
+
var name = layout.validateName(input.name), refs = refsFor(input.sessionOrigins, resolvedRefs);
|
|
132
|
+
if (!name.ok) return name;
|
|
133
|
+
if (refs.error) return { ok: false, error: refs.error };
|
|
134
|
+
var composition = validateComposition(input, refs.refs);
|
|
135
|
+
if (composition.error) return { ok: false, error: composition.error };
|
|
136
|
+
if (!uniqueMembership(prepared.record.views, refs.refs)) return { ok: false, error: "A session can belong to only one Split view" };
|
|
137
|
+
var nameMode = input.nameMode === "custom" ? "custom" : "automatic";
|
|
138
|
+
var view = { id: crypto.randomUUID(), name: nameMode === "automatic" ? names.automaticName(input.liveSessions) : name.name, nameMode: nameMode,
|
|
139
|
+
folderId: null, favorite: false, revision: prepared.record.revision + 1, layout: input.layout,
|
|
140
|
+
activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: refs.refs };
|
|
141
|
+
if (input.workspace !== undefined || refs.refs.length < 2) view.workspace = input.workspace === undefined ? workspace.legacy(view) : clone(input.workspace);
|
|
142
|
+
prepared.record.views.push(view);
|
|
143
|
+
if (input.active === true) prepared.record.activeViewId = view.id;
|
|
144
|
+
return commit(ownerId, prepared.record, view);
|
|
145
|
+
}
|
|
146
|
+
function update(ownerId, linuxUser, input, resolvedRefs) {
|
|
147
|
+
input = input || {};
|
|
148
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
149
|
+
if (!prepared.ok) return prepared;
|
|
150
|
+
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
151
|
+
if (found.error) return { ok: false, error: found.error };
|
|
152
|
+
if (found.conflict) return conflict(found.revision);
|
|
153
|
+
var identity = checkInput(input, slug, ownerId, found.view.workspace !== undefined || input.workspace !== undefined || Array.isArray(input.sessionOrigins) && input.sessionOrigins.length < 2, found.view);
|
|
154
|
+
if (!identity.ok) return identity;
|
|
155
|
+
var refs = refsFor(input.sessionOrigins, resolvedRefs);
|
|
156
|
+
if (refs.error) return { ok: false, error: refs.error };
|
|
157
|
+
if (found.view.workspace && input.workspace === undefined) return { ok: false, code: "workspace_required", error: "Update the complete workspace to preserve pane identities and layout" };
|
|
158
|
+
var ordered = names.update(found.view, refs.refs, input.liveSessions);
|
|
159
|
+
if (!ordered) return { ok: false, error: "One or more exact sessions could not be resolved" };
|
|
160
|
+
refs.refs = ordered.refs; input.liveSessions = ordered.sessions;
|
|
161
|
+
var composition = validateComposition(input, refs.refs);
|
|
162
|
+
if (composition.error) return { ok: false, error: composition.error };
|
|
163
|
+
if (input.workspace !== undefined) {
|
|
164
|
+
var transitioned = workspace.transition(workspace.normalized(found.view).workspace, input.workspace);
|
|
165
|
+
if (!transitioned.ok) return transitioned;
|
|
166
|
+
}
|
|
167
|
+
if (!uniqueMembership(prepared.record.views, refs.refs, input.id)) return { ok: false, error: "A session can belong to only one Split view" };
|
|
168
|
+
if (names.mode(found.view) === "automatic") found.view.name = names.automaticName(input.liveSessions);
|
|
169
|
+
found.view.layout = input.layout; found.view.activeSessionOriginId = input.activeSessionOriginId;
|
|
170
|
+
found.view.tree = clone(input.tree); found.view.sessionRefs = refs.refs;
|
|
171
|
+
if (input.workspace !== undefined || refs.refs.length < 2) found.view.workspace = input.workspace === undefined ? workspace.legacy(found.view) : clone(input.workspace);
|
|
172
|
+
if (input.active === true) prepared.record.activeViewId = found.view.id;
|
|
173
|
+
return commit(ownerId, prepared.record, found.view);
|
|
174
|
+
}
|
|
175
|
+
function rename(ownerId, linuxUser, input) {
|
|
176
|
+
input = input || {};
|
|
177
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
178
|
+
if (!prepared.ok) return prepared;
|
|
179
|
+
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
180
|
+
if (found.error) return { ok: false, error: found.error };
|
|
181
|
+
if (found.conflict) return conflict(found.revision);
|
|
182
|
+
var identity = checkInput(input, slug, ownerId, found.view.workspace !== undefined || input.workspace !== undefined || Array.isArray(input.sessionOrigins) && input.sessionOrigins.length < 2, found.view);
|
|
183
|
+
if (!identity.ok) return identity;
|
|
184
|
+
var name = layout.validateName(input.name);
|
|
185
|
+
if (!name.ok) return name;
|
|
186
|
+
found.view.name = name.name;
|
|
187
|
+
found.view.nameMode = "custom";
|
|
188
|
+
if (input.active === true) prepared.record.activeViewId = found.view.id;
|
|
189
|
+
return commit(ownerId, prepared.record, found.view);
|
|
190
|
+
}
|
|
191
|
+
function separate(ownerId, linuxUser, input) {
|
|
192
|
+
input = input || {};
|
|
193
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
194
|
+
if (!prepared.ok) return prepared;
|
|
195
|
+
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
196
|
+
if (found.error) return { ok: false, error: found.error };
|
|
197
|
+
if (found.conflict) return conflict(found.revision);
|
|
198
|
+
var identity = checkInput(input, slug, ownerId, found.view.workspace !== undefined || input.workspace !== undefined || Array.isArray(input.sessionOrigins) && input.sessionOrigins.length < 2, found.view);
|
|
199
|
+
if (!identity.ok) return identity;
|
|
200
|
+
var removed = prepared.record.views.splice(found.index, 1)[0];
|
|
201
|
+
if (prepared.record.activeViewId === removed.id) prepared.record.activeViewId = null;
|
|
202
|
+
return commit(ownerId, prepared.record, null);
|
|
203
|
+
}
|
|
204
|
+
function move(ownerId, linuxUser, input, resolvedSourceRefs, resolvedTargetRefs) {
|
|
205
|
+
input = input || {};
|
|
206
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
207
|
+
if (!prepared.ok) return prepared;
|
|
208
|
+
if (typeof input.sourceSessionOriginId !== "string" || !layout.validOrigin(input.sourceSessionOriginId)) return { ok: false, error: "A valid source session is required", code: "invalid" };
|
|
209
|
+
var source = validExpectedView(prepared.record, input.sourceViewId);
|
|
210
|
+
if (source.error) return { ok: false, error: "Source Split view not found", code: "not_found", revision: prepared.record.revision };
|
|
211
|
+
if ((source.view.workspace || input.workspace !== undefined) && input.targetWorkspace === undefined) return { ok: false, code: "workspace_required", error: "Legacy move does not support workspace Views" };
|
|
212
|
+
var identity = checkInput(input, slug, ownerId, !!input.targetWorkspace, source.view);
|
|
213
|
+
if (!identity.ok) return identity;
|
|
214
|
+
var sourceOrigins = source.view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
215
|
+
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 };
|
|
216
|
+
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 };
|
|
217
|
+
for (var i = 0; i < source.view.sessionRefs.length; i++) {
|
|
218
|
+
var storedSource = source.view.sessionRefs[i], liveSource = resolvedSourceRefs.find(function (ref) { return ref && ref.originId === storedSource.originId; });
|
|
219
|
+
if (!liveSource || (liveSource.ownerId || null) !== (storedSource.ownerId || null) || (liveSource.linuxUser || null) !== (storedSource.linuxUser || null)) {
|
|
220
|
+
return { ok: false, error: "A source session was replaced or changed identity", code: "session", revision: prepared.record.revision };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
var target = null, targetSessionOriginId = input.targetSessionOriginId;
|
|
224
|
+
var hasTargetView = typeof input.targetViewId === "string" && input.targetViewId.length > 0;
|
|
225
|
+
var hasStandaloneTarget = typeof targetSessionOriginId === "string" && layout.validOrigin(targetSessionOriginId);
|
|
226
|
+
if (hasTargetView === hasStandaloneTarget || (input.targetViewId != null && !hasTargetView)) {
|
|
227
|
+
return { ok: false, error: "Choose exactly one target Split view or standalone session", code: "invalid", revision: prepared.record.revision };
|
|
228
|
+
}
|
|
229
|
+
if (hasTargetView) {
|
|
230
|
+
target = validExpectedView(prepared.record, input.targetViewId);
|
|
231
|
+
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 };
|
|
232
|
+
}
|
|
233
|
+
if (target && target.view.workspace && input.targetWorkspace === undefined) return { ok: false, code: "workspace_required", error: "Legacy move does not support workspace Views" };
|
|
234
|
+
if (Array.isArray(input.targetSessionOrigins) && input.targetSessionOrigins.length > layout.MAX_SESSIONS) {
|
|
235
|
+
return { ok: false, error: "A Split view can contain at most eight sessions", code: "limit", revision: prepared.record.revision };
|
|
236
|
+
}
|
|
237
|
+
var targetRefs = refsFor(input.targetSessionOrigins, resolvedTargetRefs);
|
|
238
|
+
if (targetRefs.error) return { ok: false, error: targetRefs.error, code: "session", revision: prepared.record.revision };
|
|
239
|
+
var requestedTargetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
|
|
240
|
+
if (requestedTargetOrigins.indexOf(input.sourceSessionOriginId) === -1) return { ok: false, error: "Target layout must include the moved source session", code: "invalid", revision: prepared.record.revision };
|
|
241
|
+
if (target) {
|
|
242
|
+
var requestedPriorOrigins = target.view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
243
|
+
if (requestedTargetOrigins.length !== requestedPriorOrigins.length + 1 || requestedPriorOrigins.some(function (origin) { return requestedTargetOrigins.indexOf(origin) === -1; })) {
|
|
244
|
+
return { ok: false, error: "Target layout must preserve every existing target session", code: "invalid", revision: prepared.record.revision };
|
|
245
|
+
}
|
|
246
|
+
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 };
|
|
247
|
+
} else if (targetSessionOriginId === input.sourceSessionOriginId || requestedTargetOrigins.length !== 2 || requestedTargetOrigins.indexOf(targetSessionOriginId) === -1) {
|
|
248
|
+
return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
|
|
249
|
+
} else if (!uniqueMembership(prepared.record.views, targetRefs.refs, source.view.id)) {
|
|
250
|
+
return { ok: false, error: "A standalone target session already belongs to a Split view", code: "membership", revision: prepared.record.revision };
|
|
251
|
+
}
|
|
252
|
+
if (target) { var orderedTarget = names.append(target.view, targetRefs.refs, input.liveSessions, input.sourceSessionOriginId);
|
|
253
|
+
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 };
|
|
254
|
+
targetRefs.refs = orderedTarget.refs; input.liveSessions = orderedTarget.sessions; }
|
|
255
|
+
else { var orderedStandalone = names.targetFirst(targetRefs.refs, input.liveSessions, targetSessionOriginId, input.sourceSessionOriginId);
|
|
256
|
+
if (!orderedStandalone) return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
|
|
257
|
+
targetRefs.refs = orderedStandalone.refs; input.liveSessions = orderedStandalone.sessions; }
|
|
258
|
+
var targetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
|
|
259
|
+
if (!layout.validatePreset(input.layout) || !layout.validateTree(input.tree, targetOrigins).ok || targetOrigins.length > layout.MAX_SESSIONS) {
|
|
260
|
+
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 };
|
|
261
|
+
}
|
|
262
|
+
if (typeof input.activeSessionOriginId !== "string" || targetOrigins.indexOf(input.activeSessionOriginId) === -1) {
|
|
263
|
+
return { ok: false, error: "Active session must belong to the target Split view", code: "invalid", revision: prepared.record.revision };
|
|
264
|
+
}
|
|
265
|
+
if (!target) {
|
|
266
|
+
var name = layout.validateName(input.name);
|
|
267
|
+
if (!name.ok) return { ok: false, error: name.error, code: "invalid", revision: prepared.record.revision };
|
|
268
|
+
}
|
|
269
|
+
if (input.targetWorkspace !== undefined) {
|
|
270
|
+
if (target && input.expectedTargetViewRevision !== target.view.revision) return conflict(prepared.record.revision);
|
|
271
|
+
var checked = workspace.validate(input.targetWorkspace, { tree: input.tree, sessionRefs: targetRefs.refs });
|
|
272
|
+
if (!checked.ok) return checked;
|
|
273
|
+
var prior = target ? workspace.normalized(target.view).workspace : { panes: [] };
|
|
274
|
+
if (prior.panes.some(function (pane) { return !input.targetWorkspace.panes.some(function (next) { return workspace.samePane(next, pane); }); })) return { ok: false, code: 'workspace', error: 'Move must preserve every existing target pane and binding' };
|
|
275
|
+
var transitioned = workspace.transition(prior, input.targetWorkspace);
|
|
276
|
+
if (!transitioned.ok) return transitioned;
|
|
277
|
+
var movedPane = workspace.normalized(source.view).workspace.panes.find(function (pane) { return pane.kind === 'session' && pane.resource.originId === input.sourceSessionOriginId; });
|
|
278
|
+
var destination = input.targetWorkspace.panes.find(function (pane) { return pane.kind === 'session' && pane.resource.originId === input.sourceSessionOriginId; });
|
|
279
|
+
if (!movedPane || !destination || movedPane.instanceId !== destination.instanceId) return { ok: false, code: 'identity', error: 'Move must retain the exact pane instance' };
|
|
280
|
+
}
|
|
281
|
+
var nextSourceOrigins = sourceOrigins.filter(function (origin) { return origin !== input.sourceSessionOriginId; });
|
|
282
|
+
source.view.tree = layout.removeLeaf(source.view.tree, input.sourceSessionOriginId);
|
|
283
|
+
source.view.sessionRefs = source.view.sessionRefs.filter(function (ref) { return ref.originId !== input.sourceSessionOriginId; });
|
|
284
|
+
if (source.view.activeSessionOriginId === input.sourceSessionOriginId) source.view.activeSessionOriginId = nextSourceOrigins[0] || null;
|
|
285
|
+
workspace.removeSession(source.view, input.sourceSessionOriginId);
|
|
286
|
+
if (nextSourceOrigins.length < 2 && !source.view.workspace) { prepared.record.views.splice(source.index, 1); if (prepared.record.activeViewId === source.view.id) prepared.record.activeViewId = null; }
|
|
287
|
+
if (nextSourceOrigins.length >= 2 && names.mode(source.view) === "automatic") source.view.name = names.automaticName(input.sourceLiveSessions.filter(function (session) { return session.sessionOriginId !== input.sourceSessionOriginId; }));
|
|
288
|
+
if (target) {
|
|
289
|
+
target.view.tree = clone(input.tree); target.view.layout = input.layout;
|
|
290
|
+
target.view.activeSessionOriginId = input.activeSessionOriginId; target.view.sessionRefs = targetRefs.refs;
|
|
291
|
+
if (names.mode(target.view) === "automatic") target.view.name = names.automaticName(input.liveSessions);
|
|
292
|
+
} else {
|
|
293
|
+
target = { index: -1, view: { id: crypto.randomUUID(), folderId: null, favorite: false, name: names.automaticName(input.liveSessions), nameMode: "automatic", revision: prepared.record.revision + 1,
|
|
294
|
+
layout: input.layout, activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: targetRefs.refs } };
|
|
295
|
+
prepared.record.views.push(target.view);
|
|
296
|
+
}
|
|
297
|
+
if (input.targetWorkspace !== undefined) target.view.workspace = clone(input.targetWorkspace);
|
|
298
|
+
if (input.active === true) prepared.record.activeViewId = target.view.id;
|
|
299
|
+
var changed = [target.view];
|
|
300
|
+
if (nextSourceOrigins.length >= 2 || source.view.workspace) changed.unshift(source.view);
|
|
301
|
+
return commit(ownerId, prepared.record, changed);
|
|
302
|
+
}
|
|
303
|
+
function removeOrigin(originId) {
|
|
304
|
+
var project = projectRecord(document, false), changedOwners = [], failures = [];
|
|
305
|
+
if (!project) return { owners: changedOwners, failures: failures };
|
|
306
|
+
var keys = Object.keys(project.owners);
|
|
307
|
+
for (var i = 0; i < keys.length; i++) {
|
|
308
|
+
var source = project.owners[keys[i]], next = clone(source), changed = false, retained = [];
|
|
309
|
+
for (var j = 0; j < next.views.length; j++) {
|
|
310
|
+
var view = next.views[j], member = view.sessionRefs.some(function (ref) { return ref.originId === originId; });
|
|
311
|
+
if (!member) { retained.push(view); continue; }
|
|
312
|
+
changed = true;
|
|
313
|
+
var tree = layout.removeLeaf(view.tree, originId), origins = view.sessionRefs.filter(function (ref) { return ref.originId !== originId; });
|
|
314
|
+
if (origins.length < 2 && !view.workspace) continue;
|
|
315
|
+
view.tree = tree;
|
|
316
|
+
view.sessionRefs = origins;
|
|
317
|
+
if (view.activeSessionOriginId === originId) view.activeSessionOriginId = origins.length ? origins[0].originId : null;
|
|
318
|
+
workspace.removeSession(view, originId);
|
|
319
|
+
retained.push(view);
|
|
320
|
+
}
|
|
321
|
+
if (!changed) continue;
|
|
322
|
+
next.views = retained;
|
|
323
|
+
if (next.activeViewId && !retained.some(function (view) { return view.id === next.activeViewId; })) next.activeViewId = null;
|
|
324
|
+
next.revision++;
|
|
325
|
+
next.views.forEach(function (view) { view.revision = next.revision; });
|
|
326
|
+
try {
|
|
327
|
+
var replacement = documentForOwner(next, source.ownerId);
|
|
328
|
+
save(replacement);
|
|
329
|
+
document = replacement;
|
|
330
|
+
changedOwners.push(source.ownerId);
|
|
331
|
+
} catch (error) { failures.push({ ownerId: source.ownerId, error: error.message || String(error) }); }
|
|
332
|
+
}
|
|
333
|
+
return { owners: changedOwners, failures: failures };
|
|
334
|
+
}
|
|
335
|
+
var organization = require('./project-split-view-organization').attachOrganization({ prepare: prepareRecord, commit: commit, state: state });
|
|
336
|
+
function organize(ownerId, linuxUser, input) {
|
|
337
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
338
|
+
if (!prepared.ok) return prepared;
|
|
339
|
+
var view = prepared.record.views.find(function (item) { return item.id === input.id; });
|
|
340
|
+
var checked = checkInput(input, slug, ownerId, !!(view && view.workspace), view);
|
|
341
|
+
return checked.ok ? organization.organize(ownerId, linuxUser, input) : checked;
|
|
342
|
+
}
|
|
343
|
+
return { organize: organize, reconcileFolders: organization.reconcileFolders, state: state, create: create, update: update, rename: rename, separate: separate, move: move, removeOrigin: removeOrigin,
|
|
344
|
+
get filePath() { return filePath; }, get loadError() { return loadError; } };
|
|
345
|
+
}
|
|
346
|
+
module.exports = { createSplitViewsStore: createSplitViewsStore };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Saved descriptors are UI state, never authority to attach a terminal.
|
|
2
|
+
function attachSaved(ctx, ws, message) {
|
|
3
|
+
var binding = message.savedBinding;
|
|
4
|
+
if (!binding) return false;
|
|
5
|
+
var terminal = ctx.tm.inspect(message.id), user = ws._clayUser;
|
|
6
|
+
var osUser = ctx.getOsUserInfoForWs ? ctx.getOsUserInfoForWs(ws) : null;
|
|
7
|
+
var osIdentity = osUser ? JSON.stringify([osUser.user, osUser.uid, osUser.gid, osUser.home]) : null;
|
|
8
|
+
var allowed = terminal && !terminal.exited && terminal.kind !== 'tui-session' &&
|
|
9
|
+
binding.identity === terminal.identity && binding.runtimeId === require('./clay-control-target-projection').runtime(ctx.sm) &&
|
|
10
|
+
terminal.ownerId === (user ? user.id : null) && (terminal.osIdentity || null) === osIdentity;
|
|
11
|
+
if (user && !ctx.usersModule.getEffectivePermissions(user, ctx.osUsers).terminal) allowed = false;
|
|
12
|
+
if (!allowed) { ctx.sendTo(ws, { type: 'term_error', id: message.id, error: 'The exact saved terminal is unavailable or access changed.' }); return true; }
|
|
13
|
+
ctx.tm.attach(message.id, ws);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
module.exports = { attachSaved: attachSaved };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
var WORKBENCH_NAMES = ['project-logs', 'you', 'issues', 'linear', 'notes-browser', 'scheduled-tasks', 'debates', 'mcp-skills', 'mate-knowledge', 'wireframe'];
|
|
2
|
+
// Persisted UI descriptors confer no authority. Restore must resolve every
|
|
3
|
+
// resource through its live authenticated service; this module performs no I/O.
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
var MAX_TOOLS = 16;
|
|
6
|
+
var MAX_PANES = 24;
|
|
7
|
+
function copy(value) { return JSON.parse(JSON.stringify(value)); }
|
|
8
|
+
function object(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
|
|
9
|
+
function keys(value, fields) { return object(value) && Object.keys(value).sort().join(',') === fields.split(',').sort().join(','); }
|
|
10
|
+
function id(value) { return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value); }
|
|
11
|
+
function runtimeId(value) { return id(value) || typeof value === 'string' && /^runtime:[A-Za-z0-9_-]{1,128}$/.test(value); }
|
|
12
|
+
function filePath(value) {
|
|
13
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 4096 &&
|
|
14
|
+
!/[\\\x00-\x1f\x7f:]/.test(value) && value.split('/').every(function (part) { return part && part !== '.' && part !== '..'; });
|
|
15
|
+
}
|
|
16
|
+
function sessionId(origin) { return 'session-' + crypto.createHash('sha256').update(origin).digest('hex').slice(0, 32); }
|
|
17
|
+
function mapTree(tree, map) {
|
|
18
|
+
if (!tree) return null;
|
|
19
|
+
if (tree.id) return map[tree.id] ? { id: map[tree.id] } : null;
|
|
20
|
+
var left = mapTree(tree.left, map), right = mapTree(tree.right, map);
|
|
21
|
+
return !left ? right : !right ? left : { axis: tree.axis, ratio: tree.ratio, left: left, right: right };
|
|
22
|
+
}
|
|
23
|
+
function legacy(view) {
|
|
24
|
+
var mapping = Object.create(null), panes = view.sessionRefs.map(function (ref) {
|
|
25
|
+
var instanceId = sessionId(ref.originId); mapping[ref.originId] = instanceId;
|
|
26
|
+
return { instanceId: instanceId, kind: 'session', open: true, placement: 'docked', resource: { originId: ref.originId } };
|
|
27
|
+
});
|
|
28
|
+
return { version: 1, panes: panes, tree: mapTree(view.tree, mapping), activePaneId: mapping[view.activeSessionOriginId] || null };
|
|
29
|
+
}
|
|
30
|
+
function normalized(view) {
|
|
31
|
+
var result = copy(view);
|
|
32
|
+
if (result.workspace === undefined) result.workspace = legacy(view);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function validTree(tree, expected) {
|
|
36
|
+
var seen = [], nodes = 0;
|
|
37
|
+
function walk(node, depth) {
|
|
38
|
+
if (!object(node) || ++nodes > MAX_PANES * 2 - 1 || depth > MAX_PANES) return false;
|
|
39
|
+
if (keys(node, 'id')) {
|
|
40
|
+
if (!id(node.id) || seen.indexOf(node.id) >= 0) return false;
|
|
41
|
+
seen.push(node.id); return true;
|
|
42
|
+
}
|
|
43
|
+
return keys(node, 'axis,ratio,left,right') && ['horizontal', 'vertical'].indexOf(node.axis) >= 0 &&
|
|
44
|
+
Number.isFinite(node.ratio) && node.ratio >= .05 && node.ratio <= .95 && walk(node.left, depth + 1) && walk(node.right, depth + 1);
|
|
45
|
+
}
|
|
46
|
+
if (!expected.length) return tree === null;
|
|
47
|
+
return walk(tree, 0) && seen.length === expected.length && expected.every(function (value) { return seen.indexOf(value) >= 0; });
|
|
48
|
+
}
|
|
49
|
+
function sameTree(a, b) {
|
|
50
|
+
if (!a || !b) return a === b;
|
|
51
|
+
if (a.id || b.id) return a.id === b.id;
|
|
52
|
+
return a.axis === b.axis && a.ratio === b.ratio && sameTree(a.left, b.left) && sameTree(a.right, b.right);
|
|
53
|
+
}
|
|
54
|
+
function failure(message) { return { ok: false, code: 'workspace', error: message }; }
|
|
55
|
+
function validate(value, view) {
|
|
56
|
+
if (!object(value)) return failure('Invalid View workspace');
|
|
57
|
+
if (!keys(value, 'version,panes,tree,activePaneId' + (value.linkedPair === undefined ? '' : ',linkedPair') + (value.presentation === undefined ? '' : ',presentation')) || value.version !== 1 || !Array.isArray(value.panes) || value.panes.length > MAX_PANES) return failure('Invalid View workspace');
|
|
58
|
+
var instances = [], sessions = [], resources = [], docked = [], open = [], tools = 0, files = 0, overlays = 0, mapping = Object.create(null);
|
|
59
|
+
for (var i = 0; i < value.panes.length; i++) {
|
|
60
|
+
var pane = value.panes[i];
|
|
61
|
+
if (!keys(pane, 'instanceId,kind,open,placement,resource') || !id(pane.instanceId) || instances.indexOf(pane.instanceId) >= 0 ||
|
|
62
|
+
typeof pane.open !== 'boolean' || ['docked', 'overlay'].indexOf(pane.placement) < 0) return failure('Invalid or duplicate pane instance');
|
|
63
|
+
instances.push(pane.instanceId);
|
|
64
|
+
var resource = pane.resource;
|
|
65
|
+
if (pane.kind === 'session') {
|
|
66
|
+
if (!keys(resource, 'originId') || !id(resource.originId) || sessions.indexOf(resource.originId) >= 0 || !pane.open || pane.placement !== 'docked') return failure('Invalid session pane');
|
|
67
|
+
sessions.push(resource.originId); mapping[pane.instanceId] = resource.originId;
|
|
68
|
+
} else {
|
|
69
|
+
if (++tools > MAX_TOOLS) return failure('A View supports at most sixteen tool panes');
|
|
70
|
+
if (pane.kind === 'terminal' || pane.kind === 'browser') {
|
|
71
|
+
var terminal = pane.kind === 'terminal';
|
|
72
|
+
if (terminal ? !keys(resource, 'id,runtimeId,identity') || !Number.isSafeInteger(resource.id) || resource.id < 1 || !id(resource.identity) || !runtimeId(resource.runtimeId) :
|
|
73
|
+
!keys(resource, 'id,runtimeId,sessionOriginId') || !id(resource.id) || !runtimeId(resource.runtimeId) || !id(resource.sessionOriginId)) return failure('Invalid runtime resource descriptor');
|
|
74
|
+
var identity = pane.kind + ':' + resource.runtimeId + ':' + resource.id;
|
|
75
|
+
if (resources.indexOf(identity) >= 0) return failure('Duplicate runtime resource');
|
|
76
|
+
resources.push(identity);
|
|
77
|
+
} else if (pane.kind === 'file') {
|
|
78
|
+
if (++files > 8) return failure('A View supports at most eight file panes');
|
|
79
|
+
if (!keys(resource, 'path,mode,rendered') || !filePath(resource.path) || ['follow', 'pin'].indexOf(resource.mode) < 0 || typeof resource.rendered !== 'boolean') return failure('Invalid file resource descriptor');
|
|
80
|
+
} else if (pane.kind === 'workbench') {
|
|
81
|
+
if (!keys(resource, 'name') || WORKBENCH_NAMES.indexOf(resource.name) < 0 || resources.indexOf('workbench:' + resource.name) >= 0) return failure('Invalid or duplicate workbench');
|
|
82
|
+
resources.push('workbench:' + resource.name);
|
|
83
|
+
} else return failure('Unknown pane kind');
|
|
84
|
+
}
|
|
85
|
+
if (pane.open) { open.push(pane.instanceId); if (pane.placement === 'docked') docked.push(pane.instanceId); else overlays++; }
|
|
86
|
+
}
|
|
87
|
+
var origins = view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
88
|
+
if (sessions.length !== origins.length || origins.some(function (origin) { return sessions.indexOf(origin) < 0; })) return failure('Workspace sessions must exactly match View membership');
|
|
89
|
+
if (overlays > 1 || !validTree(value.tree, docked)) return failure('Workspace layout must exactly match open docked panes');
|
|
90
|
+
if (open.length ? open.indexOf(value.activePaneId) < 0 : value.activePaneId !== null) return failure('Active pane must be open');
|
|
91
|
+
if (!sameTree(mapTree(value.tree, mapping), view.tree)) return failure('Session tree must match the workspace session projection');
|
|
92
|
+
if (value.presentation !== undefined && (!keys(value.presentation, 'focusedSessionOriginId,expandedPaneId') || value.presentation.focusedSessionOriginId !== null && sessions.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 failure('Invalid View presentation');
|
|
93
|
+
if (value.linkedPair !== undefined && !require('./project-view-pair').valid(value.linkedPair, value, view)) return failure('Invalid linked Worker presentation');
|
|
94
|
+
return { ok: true };
|
|
95
|
+
}
|
|
96
|
+
function samePane(a, b) {
|
|
97
|
+
return a.instanceId === b.instanceId && a.kind === b.kind && a.open === b.open && a.placement === b.placement &&
|
|
98
|
+
Object.keys(a.resource).length === Object.keys(b.resource).length && Object.keys(a.resource).every(function (key) { return a.resource[key] === b.resource[key]; });
|
|
99
|
+
}
|
|
100
|
+
function transition(previous, next) {
|
|
101
|
+
for (var i = 0; i < next.panes.length; i++) {
|
|
102
|
+
var pane = next.panes[i], prior = previous.panes.find(function (item) { return item.instanceId === pane.instanceId; });
|
|
103
|
+
if (!prior) continue;
|
|
104
|
+
if (pane.kind !== prior.kind || pane.kind !== 'file' && Object.keys(pane.resource).some(function (key) { return pane.resource[key] !== prior.resource[key]; })) return failure('An existing pane instance cannot change resource identity');
|
|
105
|
+
}
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
// Session deletion never discards another pane or its binding. The legacy
|
|
109
|
+
// session tree is a compatibility projection of the authoritative workspace.
|
|
110
|
+
function removeSession(view, origin) {
|
|
111
|
+
if (!view.workspace) return;
|
|
112
|
+
if (view.workspace.linkedPair && view.workspace.linkedPair.driverOriginId === origin) delete view.workspace.linkedPair;
|
|
113
|
+
var value = view.workspace, mapping = Object.create(null), sessionMapping = Object.create(null);
|
|
114
|
+
value.panes = value.panes.filter(function (pane) { return pane.kind !== 'session' || pane.resource.originId !== origin; });
|
|
115
|
+
value.panes.forEach(function (pane) { mapping[pane.instanceId] = pane.instanceId; if (pane.kind === 'session') sessionMapping[pane.instanceId] = pane.resource.originId; });
|
|
116
|
+
value.tree = mapTree(value.tree, mapping);
|
|
117
|
+
view.tree = mapTree(value.tree, sessionMapping);
|
|
118
|
+
if (value.presentation && value.presentation.focusedSessionOriginId === origin) value.presentation.focusedSessionOriginId = null;
|
|
119
|
+
if (!value.panes.some(function (pane) { return pane.open && pane.instanceId === value.activePaneId; })) {
|
|
120
|
+
var active = value.panes.find(function (pane) { return pane.open; }); value.activePaneId = active ? active.instanceId : null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
module.exports = { samePane: samePane, MAX_TOOLS: MAX_TOOLS, MAX_PANES: MAX_PANES, validate: validate, transition: transition,
|
|
124
|
+
normalized: normalized, legacy: legacy, removeSession: removeSession, validFilePath: filePath };
|
|
125
|
+
|
|
126
|
+
module.exports.workbenchNames = WORKBENCH_NAMES;
|
package/lib/project.js
CHANGED
|
@@ -699,6 +699,7 @@ function createProjectContext(opts) {
|
|
|
699
699
|
},
|
|
700
700
|
});
|
|
701
701
|
var _splitViews = attachProjectSplitViews({
|
|
702
|
+
getViewPairGroup: function (id) { return _splitGroups.store.groupForMember(id); },
|
|
702
703
|
sm: sm, slug: slug, clients: clients, sendTo: sendTo, usersModule: usersModule,
|
|
703
704
|
isMate: isMate, osUsers: osUsers,
|
|
704
705
|
canAccessProject: function (ws) { return _requestAccess.canAccessProject(ws, slug); },
|
package/lib/public/app.js
CHANGED
|
@@ -507,6 +507,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
507
507
|
savedMainSlug: null,
|
|
508
508
|
connected: false,
|
|
509
509
|
fileReadRequest: null,
|
|
510
|
+
filePaneInstances: {},
|
|
511
|
+
activeFilePaneId: null,
|
|
510
512
|
fileViewerIsPlantUml: false,
|
|
511
513
|
plantUmlPreviewJob: null,
|
|
512
514
|
wireframeDraft: null,
|
|
@@ -155,3 +155,6 @@
|
|
|
155
155
|
.issue-github-matches a { flex: 1; min-width: 0; color: var(--text); text-decoration: none; overflow-wrap: anywhere; }
|
|
156
156
|
.issue-github-matches a:hover { text-decoration: underline; }
|
|
157
157
|
.issue-github-matches small { display: block; color: var(--text-muted); margin-bottom: 3px; }
|
|
158
|
+
|
|
159
|
+
#issues-panel .issues-linear-link { display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto; height: 30px; padding: 0 8px; color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
|
|
160
|
+
#issues-panel .issues-linear-link .lucide { width: 12px; height: 12px; }
|
package/lib/public/css/pane.css
CHANGED
|
@@ -55,6 +55,11 @@ body.pane-mode #input-area {
|
|
|
55
55
|
position: relative;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/* The workspace owns the canvas color behind every pane and expanded tool. */
|
|
59
|
+
#main-panels:has(> #split-host.split-view-canvas.visible) {
|
|
60
|
+
background: var(--bg-alt);
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
#app.split-native-hidden {
|
|
59
64
|
display: none !important;
|
|
60
65
|
}
|
|
@@ -65,7 +70,6 @@ body.pane-mode #input-area {
|
|
|
65
70
|
flex: 1;
|
|
66
71
|
min-width: 0;
|
|
67
72
|
min-height: 0;
|
|
68
|
-
background: var(--bg);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
#split-host.visible {
|
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
#main-panels > [data-workbench-placement="docked"] :is(.terminal-header, .file-viewer-tabbar, .shared-browser-viewport) {
|
|
76
76
|
background: var(--bg);
|
|
77
77
|
}
|
|
78
|
-
#main-panels > #terminal-container[data-workbench-placement="docked"] #terminal-body { background: var(--bg); }
|
|
79
78
|
}
|
|
80
79
|
@media (max-width: 1023px) {
|
|
81
80
|
#main-panels > [data-workbench-placement="docked"][data-workbench-active="false"] { display: none !important; }
|
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
.scheduled-tasks-create-menu button:hover, .scheduled-tasks-create-menu button:focus-visible { outline: 0; background: var(--bg-alt); }
|
|
30
30
|
.scheduled-task-default-ai { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 14px; border-bottom: 1px solid var(--border-subtle); background: var(--bg-alt); color: var(--text-dimmer); font-size: 11px; }
|
|
31
31
|
.scheduled-task-default-ai button { border: 0; background: transparent; color: var(--accent); font: inherit; font-weight: 700; cursor: pointer; }
|
|
32
|
+
.scheduled-tasks-toolbar { display: flex; align-items: center; gap: 12px; padding: 5px 8px; border-bottom: 1px solid var(--border-subtle); background: var(--bg-alt); flex-shrink: 0; }
|
|
33
|
+
.scheduled-tasks-toolbar .scheduled-task-default-ai { flex: 1; min-width: 0; gap: 8px; padding: 0; border: 0; flex-wrap: wrap; }
|
|
34
|
+
.scheduled-tasks-toolbar .scheduled-task-default-ai span { flex: 1; min-width: 120px; }
|
|
35
|
+
.scheduled-tasks-toolbar .scheduled-task-default-ai button { padding: 0; white-space: nowrap; }
|
|
36
|
+
.scheduled-tasks-toolbar .scheduled-tasks-create-group { height: 24px; }
|
|
37
|
+
.scheduled-tasks-toolbar .scheduled-tasks-new { height: 24px; gap: 4px; padding: 0 7px; font-size: 11px; font-weight: 500; border-radius: 5px 0 0 5px; }
|
|
38
|
+
.scheduled-tasks-toolbar .scheduled-tasks-new-menu { height: 24px; width: 23px; border-radius: 0 5px 5px 0; }
|
|
32
39
|
.scheduled-tasks-list { flex: 1; min-height: 0; overflow-y: auto; }
|
|
33
40
|
.scheduled-task-row { border-bottom: 1px solid var(--border-subtle); border-left: 2px solid transparent; }
|
|
34
41
|
.scheduled-task-row:not(.paused) { border-left-color: var(--brand-green); }
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
#split-host.split-view-canvas.visible {
|
|
80
80
|
display: flex;
|
|
81
81
|
padding: 7px;
|
|
82
|
-
background: var(--bg-alt);
|
|
83
82
|
box-sizing: border-box;
|
|
84
83
|
}
|
|
85
84
|
.split-view-tree, .split-view-branch, .split-view-leaf {
|
|
@@ -107,8 +106,10 @@
|
|
|
107
106
|
}
|
|
108
107
|
.split-view-canvas .split-pane-title {
|
|
109
108
|
min-width: 0;
|
|
110
|
-
flex: 1;
|
|
109
|
+
flex: 0 1 auto;
|
|
111
110
|
max-width: none;
|
|
111
|
+
padding: 0;
|
|
112
|
+
margin-left: 0;
|
|
112
113
|
font-size: 11px;
|
|
113
114
|
font-weight: 500;
|
|
114
115
|
overflow: hidden;
|
|
@@ -174,7 +175,7 @@ body.split-view-resizing .split-pane-frame { pointer-events: none; }
|
|
|
174
175
|
align-content: center;
|
|
175
176
|
gap: 4px;
|
|
176
177
|
}
|
|
177
|
-
.split-view-canvas .split-pane-title { flex: 1
|
|
178
|
+
.split-view-canvas .split-pane-title { flex: 0 1 auto; }
|
|
178
179
|
.split-view-canvas .split-view-runtime { flex: 0 1 auto; max-width: 55%; min-width: 48px; }
|
|
179
180
|
}
|
|
180
181
|
.split-view-workers {
|
|
@@ -462,3 +463,36 @@ body.split-view-resizing .split-pane-frame { pointer-events: none; }
|
|
|
462
463
|
|
|
463
464
|
/* Light reflects received WebSocket output, independent of processing state. */
|
|
464
465
|
.split-view-worker-dot.io { background: var(--brand-green); box-shadow: 0 0 5px var(--brand-green); }
|
|
466
|
+
|
|
467
|
+
/* Header whitespace is a handle; text and controls retain their own actions. */
|
|
468
|
+
.split-pane-drag-space { flex: 1 1 12px; min-width: 12px; align-self: stretch; cursor: grab; touch-action: none; }
|
|
469
|
+
.split-view-canvas .split-pane-header { cursor: grab; touch-action: none; }
|
|
470
|
+
.split-view-canvas .split-pane-header[data-pane-loading="true"] { cursor: default; }
|
|
471
|
+
body.is-dragging .split-pane-header, body.is-dragging .split-pane-drag-space { cursor: grabbing; }
|
|
472
|
+
.split-view-canvas.split-view-drag-preview > .split-pane,
|
|
473
|
+
.split-view-canvas.split-view-drag-preview > .split-view-divider,
|
|
474
|
+
.split-view-canvas.split-view-drag-preview > .split-view-drop-preview {
|
|
475
|
+
transition: left 280ms cubic-bezier(.2,.75,.25,1), top 280ms cubic-bezier(.2,.75,.25,1),
|
|
476
|
+
width 280ms cubic-bezier(.2,.75,.25,1), height 280ms cubic-bezier(.2,.75,.25,1);
|
|
477
|
+
}
|
|
478
|
+
.split-view-canvas.split-view-drag-preview > .split-view-divider { pointer-events: none; opacity: .35; }
|
|
479
|
+
.split-view-canvas.split-view-drag-preview > .split-view-drop-preview { background: transparent; }
|
|
480
|
+
@media (prefers-reduced-motion: reduce) {
|
|
481
|
+
.split-view-canvas.split-view-drag-preview > .split-pane,
|
|
482
|
+
.split-view-canvas.split-view-drag-preview > .split-view-divider,
|
|
483
|
+
.split-view-canvas.split-view-drag-preview > .split-view-drop-preview { transition: none; }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.split-view-canvas > .split-pane.split-pane-drag-source { opacity: .72; }
|
|
487
|
+
|
|
488
|
+
/* A shared tint marks the moving window without altering its content theme. */
|
|
489
|
+
:is(.split-pane-drag-source, .tool-pane-drag-source)::after {
|
|
490
|
+
content: "";
|
|
491
|
+
position: absolute;
|
|
492
|
+
inset: 0;
|
|
493
|
+
z-index: 20;
|
|
494
|
+
border-radius: inherit;
|
|
495
|
+
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
|
496
|
+
box-shadow: inset 0 0 0 1px var(--accent-20);
|
|
497
|
+
pointer-events: none;
|
|
498
|
+
}
|