lib/clay-split-view-controls.js
Rama main · réplica de lectura del repositorio original.
var workspaceInput = require('./clay-view-workspace-input');
var crypto = require('crypto'), fs = require('fs');
var layout = require('./split-view-layout');
var schema = require('./clay-split-view-input').operationSchema;
function attachSplitViewControls(ctx) {
function live(op) {
var principal = ctx.authorize(), project = ctx.getProjects().get(op.projectSlug), status = project && project.getStatus();
if (!status || status.isMate) throw new Error('Select an existing ordinary project.');
ctx.authorizeProject(principal, status);
var viewer = ctx.service().inspect(principal.userId, op.viewerRef);
if (!viewer.supported || viewer.slug !== op.projectSlug) throw new Error('Select an authenticated owner window already connected to this project.');
var api = project.getSplitViewControl && project.getSplitViewControl(viewer.ws);
if (!api) throw new Error('Split view controls are unavailable for this connection.');
var saved = api.state();
if (!saved || saved.error || saved.ok === false) throw new Error(saved && saved.error || 'Split view state is unavailable.');
var sessions = project.getSessionManager().sessions;
function exact(origin) {
var matches = Array.from(sessions.values()).filter(function (item) { return item.sessionOriginId === origin; });
var session = matches.length === 1 && matches[0];
if (!session || session.hidden || session.destroying || session.spawn || require('./session-provenance').isWorker(session) || (session.ownerId || null) !== (principal.userId || null)) throw new Error('An exact owned presentation member is unavailable.');
return session;
}
saved.views.forEach(function (view) { view.sessions.forEach(function (item) { exact(item.sessionOriginId); }); });
var origins = op.sessionOrigins || op.targetSessionOrigins || [];
var members = origins.map(exact);
var view = saved.views.find(function (item) { return item.id === (op.id || op.sourceViewId); });
if (op.change) {
if (op.accountId !== principal.userId) throw new Error('Exact account identity changed.');
if (JSON.stringify(op).length > workspaceInput.MAX_OPERATION_BYTES) throw new Error('Bounded View composition is too large.');
workspaceInput.validateOperation(op, saved, view);
if (saved.revision !== op.expectedRevision) throw new Error('Saved view collection revision changed.');
if (op.change !== 'create' && (!view || view.revision !== op.expectedViewRevision)) throw new Error('Exact Split view revision changed.');
if ((op.change === 'open' || op.change === 'focus') && op.focusOrigin && !view.sessions.some(function (item) { return item.sessionOriginId === op.focusOrigin; })) throw new Error('Focus must belong to the exact view.');
if (['create', 'update', 'move'].indexOf(op.change) >= 0 && (new Set(origins).size !== origins.length || !layout.validateTree(op.tree, origins).ok)) throw new Error('Layout must contain exactly zero to eight unique members.');
}
var directory = fs.statSync(status.path);
return { project: project, root: JSON.stringify([status.path, fs.realpathSync(status.path), directory.dev, directory.ino]), principal: principal, viewer: viewer, api: api, saved: saved, view: view,
identity: JSON.stringify(saved.owner), members: members.concat(view ? view.sessions.map(function (item) { return exact(item.sessionOriginId); }) : []) };
}
function check(entry, afterNavigation) {
if (Date.now() - entry.created > 300000) throw new Error('Split view inspection expired.');
var current = live(entry.op), initial = entry.snapshot;
if (current.project !== initial.project || current.root !== initial.root || current.identity !== initial.identity || current.members.length !== initial.members.length || current.members.some(function (item, i) { return item !== initial.members[i]; })) throw new Error('Split view project, OS identity or members changed.');
if (!afterNavigation) ctx.service().check(current.principal.userId, initial.viewer);
return current;
}
async function read(args) {
var current = live(args);
var identities = current.saved.views.flatMap(function (view) { return view.sessions.map(function (item) { return current.project.getSessionManager().sessions.get(item.id); }); });
var workerReads = new Map();
var active = current.project.getSessionManager().sessions.get(current.viewer.sessionId);
identities.concat(active || []).forEach(function (session) {
if (!session || require('./session-provenance').isWorker(session) || workerReads.has(session.sessionOriginId)) return;
workerReads.set(session.sessionOriginId, current.project.getViewWorkerSupervision(current.viewer.ws, session.sessionOriginId));
});
function validateRead(next) {
ctx.service().check(next.principal.userId, current.viewer);
if (next.project !== current.project || next.root !== current.root || next.identity !== current.identity || identities.some(function (session) {
return !session || next.project.getSessionManager().sessions.get(session.localId) !== session || (session.ownerId || null) !== (next.principal.userId || null);
}) || next.saved.revision !== current.saved.revision) throw new Error('Project or exact view member identity changed.');
workerReads.forEach(function (reader) { reader.revalidate(); });
}
var observed = await ctx.service().observeView(current.principal.userId, current.viewer, function () {
validateRead(live(args));
});
var latest = live(args); validateRead(latest); workspaceInput.validateObservation(latest.saved, observed);
var awareness = require('./clay-split-view-awareness').describe(latest, observed, args.projectSlug, workerReads);
return { projectSlug: args.projectSlug, accountId: latest.principal.userId, workspaceVersion: 1, maxToolPanes: 8, reviewLimits: { operationCharacters: workspaceInput.MAX_OPERATION_BYTES, questionCharacters: 2000 }, revision: latest.saved.revision, views: awareness.views, currentView: awareness.currentView, runtimeObservedAt: awareness.runtimeObservedAt, folders: latest.api.folders(),
ownerWindow: { viewerRef: args.viewerRef, connection: 'connected', observedAt: Date.now(), evidence: 'Exact authenticated server socket; independent of provider runtime and pane readiness.' },
screen: observed, maxMembers: 8, presets: layout.PRESETS, untrusted: true,
unsupported: { linkedWorkerPresentation: 'Canonical composition may retain linked Worker layout but cannot open, close or retarget the pair; use human Worker controls. Render proof for navigation with visible linked Worker frames remains unavailable.', newToolResource: 'View composition cannot create or rebind file, Terminal or Browser resources. Use a separately authorized typed resource action, then the human View controls.', workbenchMode: 'Account-wide User Settings preference, excluded from canonical controls.', workerExecution: 'Use reviewed project Driver assignment.', offlineWindow: 'Select a live owner window connected to the project.' } };
}
async function inspect(args) {
if (JSON.stringify(args.operation).length > workspaceInput.MAX_OPERATION_BYTES) throw new Error('Bounded View composition is too large.');
var op = schema.parse(args.operation), current = live(op);
var observed = await ctx.service().observeView(current.principal.userId, current.viewer, function () { live(op); });
if (observed.status !== 'observed' || observed.snapshot.hidden) throw new Error('The selected window did not provide a visible stable screen observation.' + (observed.error ? ' ' + observed.error : ''));
workspaceInput.validateObservation(current.saved, observed);
if ((op.workspace || op.targetWorkspace || current.view && (current.view.sessions.length < 2 || current.view.workspace && current.view.workspace.panes.some(function (pane) { return pane.kind !== 'session'; }))) && !Array.isArray(observed.snapshot.tools)) throw new Error('The selected client does not support workspace observations.');
var entry = { slug: op.projectSlug, op: op, snapshot: current, screen: observed.snapshot, created: Date.now() };
check(entry);
if (op.change === 'focus' && entry.screen.viewId !== op.id) throw new Error('Focus requires that exact view already open in the selected window.');
entry.presentationValid = function () { check(entry); };
var token = 'split-view-control:' + crypto.randomUUID();
if (ctx.state.targets.size >= 100) ctx.state.targets.delete(ctx.state.targets.keys().next().value);
ctx.state.targets.set(token, entry);
return { targetToken: token, operation: op, savedView: current.view || null, selectedScreen: entry.screen, expiresInSeconds: 300 };
}
function act(args, entry) {
ctx.authorize();
var key = JSON.stringify([args.targetToken, args.why]), old = ctx.state.requests.get(args.requestId);
if (old) {
if (old.key !== key) throw new Error('requestId names another exact intent.');
if (Date.now() - old.created > 300000) throw new Error('Five-minute receipt expired. Inspect before any new reviewed action.');
// A completed receipt is historical; never replay a persisted mutation.
var principal = ctx.authorize(), project = ctx.getProjects().get(entry.op.projectSlug);
if (project !== entry.snapshot.project || JSON.stringify([project.getStatus().path, fs.realpathSync(project.getStatus().path), fs.statSync(project.getStatus().path).dev, fs.statSync(project.getStatus().path).ino]) !== entry.snapshot.root) throw new Error('Target project was replaced.');
ctx.authorizeProject(principal, project.getStatus());
var viewer = ctx.service().inspect(principal.userId, entry.op.viewerRef);
var api = project.getSplitViewControl(viewer.ws), saved = api && api.state();
if (!saved || saved.error || JSON.stringify(saved.owner) !== entry.snapshot.identity || entry.snapshot.members.some(function (item) { return project.getSessionManager().sessions.get(item.localId) !== item || (item.ownerId || null) !== (principal.userId || null); })) throw new Error('Target owner, OS identity or members changed.');
return old.promise;
}
if (ctx.state.requests.size >= 100) throw new Error('Too many screen requests in this query.');
var promise = Promise.resolve().then(async function () {
check(entry);
var question = 'Change Split view: ' + entry.op.change + '\n' + workspaceInput.reviewText(entry.op, entry.op.targetViewId ? entry.snapshot.saved.views.find(function (view) { return view.id === entry.op.targetViewId; }) : entry.snapshot.view) + '\nWhy: ' + args.why;
if (question.length > 2000) throw new Error('Exact view change must fit the owner review.');
var answer = await ctx.review(question, function () { check(entry); }, { kind: 'product-control', action: 'Change Split view', target: entry.snapshot.view ? entry.snapshot.view.name : 'New Split view', context: 'Selected window in ' + entry.op.projectSlug, reason: args.why });
if (answer.status !== 'submitted' || !answer.answers.control || answer.answers.control.length !== 1 || answer.answers.control[0] !== 'Apply exact change') return { status: 'cancelled', persisted: false };
var current = check(entry);
var observed = await ctx.service().observeView(current.principal.userId, current.viewer, function () { check(entry); });
current = check(entry);
workspaceInput.validateObservation(current.saved, observed);
if (observed.status !== 'observed' || JSON.stringify(observed.snapshot) !== JSON.stringify(entry.screen)) throw new Error('Selected screen changed during review. Inspect again.');
if (entry.op.change === 'open' || entry.op.change === 'focus') {
var target = { surface: 'split_view', projectSlug: entry.op.projectSlug, view: current.view, focusOrigin: entry.op.focusOrigin, expectedScreen: entry.screen };
return ctx.service().deliver(current.principal.userId, current.viewer, target, function () { check(entry, true); }, ctx.activity && ctx.activity());
}
var result = current.api[entry.op.change](Object.assign({}, entry.op));
if (!result.ok) return { status: 'failed', persisted: false, error: result.error, code: result.code };
try { current.api.publish(); } catch (error) { return { status: 'failed', persisted: true, revision: result.revision, visible: 'unknown', retrySafe: false, error: error.message }; }
return { status: 'persisted', persisted: true, broadcast: 'attempted', uiObserved: false, revision: result.revision, view: result.view, visible: 'unknown', sessionsDeleted: false, executionChanged: false };
}).catch(function (error) { return { status: 'failed', retrySafe: false, error: error.message }; });
ctx.state.requests.set(args.requestId, { key: key, promise: promise, created: Date.now() }); return promise;
}
return { read: read, inspect: inspect, act: act };
}
module.exports = { attachSplitViewControls: attachSplitViewControls };