clay-server 5.3.0-beta.4 → 5.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ask-user-request-identity.js +18 -1
- package/lib/clay-product-control-tools.js +2 -2
- package/lib/clay-product-control.js +1 -1
- package/lib/clay-screen-controls.js +1 -0
- package/lib/clay-split-view-awareness.js +5 -2
- package/lib/clay-split-view-controls.js +14 -7
- package/lib/clay-split-view-input.js +5 -4
- package/lib/clay-split-view-render-proof.js +14 -4
- package/lib/clay-view-workspace-input.js +71 -0
- package/lib/project-delegated-follow-up.js +18 -9
- package/lib/project-file-history.js +11 -9
- package/lib/project-file-watch.js +26 -7
- package/lib/project-filesystem.js +5 -4
- package/lib/project-shared-browser.js +7 -3
- package/lib/project-split-view-document.js +16 -2
- package/lib/project-split-views.js +15 -294
- package/lib/project-user-message.js +4 -3
- package/lib/project-vendor-login.js +2 -2
- package/lib/project-view-input.js +11 -0
- package/lib/project-view-pair.js +26 -0
- package/lib/project-view-store.js +346 -0
- package/lib/project-view-terminal-binding.js +16 -0
- package/lib/project-view-workspace.js +126 -0
- package/lib/project.js +1 -0
- package/lib/public/app.js +2 -0
- package/lib/public/css/issues.css +3 -0
- package/lib/public/css/pane.css +5 -1
- package/lib/public/css/right-workbench.css +25 -3
- package/lib/public/css/scheduled-tasks.css +7 -0
- package/lib/public/css/shared-browser.css +21 -0
- package/lib/public/css/split-view-layout.css +37 -3
- package/lib/public/css/sticky-notes.css +3 -2
- package/lib/public/css/tool-pane.css +127 -0
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-header.js +24 -2
- package/lib/public/modules/app-messages.js +1 -1
- package/lib/public/modules/clay-file-links.js +2 -1
- package/lib/public/modules/clay-session-links.js +1 -1
- package/lib/public/modules/clay-split-view-screen.js +6 -3
- package/lib/public/modules/clay-tool-pane-screen.js +35 -0
- package/lib/public/modules/file-history-snapshot.js +12 -0
- package/lib/public/modules/file-pane-entrypoints.js +55 -0
- package/lib/public/modules/file-pane-history.js +78 -0
- package/lib/public/modules/file-pane-instances.js +140 -0
- package/lib/public/modules/file-pane-links.js +41 -0
- package/lib/public/modules/file-pane-render.js +48 -0
- package/lib/public/modules/file-pane-review.js +26 -0
- package/lib/public/modules/file-pane-toolbar.js +54 -0
- package/lib/public/modules/file-viewer-source.js +70 -0
- package/lib/public/modules/filebrowser.js +44 -83
- package/lib/public/modules/git-panel.js +5 -0
- package/lib/public/modules/issues.js +3 -4
- package/lib/public/modules/linear-panel.js +2 -2
- package/lib/public/modules/mate-knowledge-workbench.js +10 -6
- package/lib/public/modules/notifications.js +4 -1
- package/lib/public/modules/pane-activation.js +5 -0
- package/lib/public/modules/pane-binding.js +5 -1
- package/lib/public/modules/pane-browser-bridge.js +20 -6
- package/lib/public/modules/pane-browser-transport.js +110 -0
- package/lib/public/modules/pane-header-runtime.js +18 -0
- package/lib/public/modules/plantuml-preview.js +12 -12
- package/lib/public/modules/right-workbench.js +116 -67
- package/lib/public/modules/scheduled-tasks.js +7 -6
- package/lib/public/modules/shared-browser.js +75 -9
- package/lib/public/modules/split-pane-renderer.js +7 -8
- package/lib/public/modules/split-pane-runtime.js +23 -0
- package/lib/public/modules/split-view-drag-preview.js +90 -0
- package/lib/public/modules/split-view-layout-client.js +1 -1
- package/lib/public/modules/split-view-presentation-drag.js +36 -13
- package/lib/public/modules/split-view-presentation-drop.js +21 -18
- package/lib/public/modules/split-view-presentation-renderer.js +15 -10
- package/lib/public/modules/split-view-presentations.js +25 -33
- package/lib/public/modules/split-view.js +17 -16
- package/lib/public/modules/sticky-notes-browser.js +2 -2
- package/lib/public/modules/sticky-notes.js +8 -10
- package/lib/public/modules/terminal.js +45 -13
- package/lib/public/modules/theme.js +1 -1
- package/lib/public/modules/tool-pane-chrome.js +82 -0
- package/lib/public/modules/tool-pane-drag.js +134 -0
- package/lib/public/modules/tool-pane-layout.js +95 -0
- package/lib/public/modules/tool-pane-legacy.js +60 -0
- package/lib/public/modules/tool-pane-tree.js +51 -0
- package/lib/public/modules/view-legacy-coordinates.js +16 -0
- package/lib/public/modules/view-mutations.js +24 -0
- package/lib/public/modules/view-pair-presentation.js +74 -0
- package/lib/public/modules/view-pair-validation.js +12 -0
- package/lib/public/modules/view-presentation-state.js +37 -0
- package/lib/public/modules/view-workspace-controller.js +210 -0
- package/lib/public/modules/view-workspace-state.js +103 -0
- package/lib/public/modules/view-workspace-validation.js +47 -0
- package/lib/public/modules/wireframe-workbench.js +11 -0
- package/lib/public/modules/workbench-layout-preference.js +12 -5
- package/lib/public/modules/workbench-panels.js +21 -0
- package/lib/public/modules/workspace-assignment-card.js +9 -6
- package/lib/public/pane-assets/{app-ED2VCGNQ.js → app-7UZ727NA.js} +1 -1
- package/lib/public/pane-assets/chunk-2AIWCI6Y.js +1 -0
- package/lib/public/pane-assets/chunk-6MIHIQ2W.js +1 -0
- package/lib/public/pane-assets/chunk-6NYCNWVF.js +1 -0
- package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
- package/lib/public/pane-assets/chunk-6UJ2ALOB.js +1 -0
- package/lib/public/pane-assets/chunk-6XK2OATZ.js +1 -0
- package/lib/public/pane-assets/chunk-BKIDR2ME.js +1 -0
- package/lib/public/pane-assets/chunk-BMHTBRMO.js +1 -0
- package/lib/public/pane-assets/chunk-GEFCFZEN.js +1 -0
- package/lib/public/pane-assets/chunk-GN2NQEVC.js +2 -0
- package/lib/public/pane-assets/chunk-IMLMBPT4.js +166 -0
- package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
- package/lib/public/pane-assets/chunk-K5S6AILM.js +1 -0
- package/lib/public/pane-assets/chunk-LDIZLKB2.js +1 -0
- package/lib/public/pane-assets/chunk-OWNNLZ36.js +1 -0
- package/lib/public/pane-assets/chunk-P2C3LOWL.js +3 -0
- package/lib/public/pane-assets/chunk-PV4PCH2V.js +2 -0
- package/lib/public/pane-assets/chunk-R4LMPTKU.js +1 -0
- package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
- package/lib/public/pane-assets/chunk-TMGSDFJI.js +1 -0
- package/lib/public/pane-assets/chunk-VKYBAVWT.js +1 -0
- package/lib/public/pane-assets/chunk-VNAV23CM.js +1 -0
- package/lib/public/pane-assets/chunk-VWTHIQCE.js +1 -0
- package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
- package/lib/public/pane-assets/chunk-WM3K5AFH.js +1 -0
- package/lib/public/pane-assets/chunk-XOB3O327.js +1 -0
- package/lib/public/pane-assets/chunk-XOOUCFYT.js +1 -0
- package/lib/public/pane-assets/manifest.json +383 -336
- package/lib/public/pane-assets/style-ZM7OOU3M.css +1 -0
- package/lib/public/style.css +1 -0
- package/lib/server-workbench-layout-settings.js +3 -3
- package/lib/server.js +1 -0
- package/lib/split-view-layout.js +11 -7
- package/lib/users-workbench-layout-preferences.js +14 -4
- package/lib/workbench-window-layout.js +30 -0
- package/lib/workspace-assignment-service.js +56 -11
- package/lib/workspace-follow-up-queue.js +92 -0
- package/lib/workspace-query-mcp-server.js +1 -1
- package/package.json +1 -1
- package/lib/public/pane-assets/chunk-6D4DPE5X.js +0 -1
- package/lib/public/pane-assets/chunk-6SM2WQJX.js +0 -1
- package/lib/public/pane-assets/chunk-7H36SUZA.js +0 -2
- package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
- package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
- package/lib/public/pane-assets/chunk-IYFILQHZ.js +0 -2
- package/lib/public/pane-assets/chunk-IZ2ZANEY.js +0 -167
- package/lib/public/pane-assets/chunk-JAEQCN5S.js +0 -1
- package/lib/public/pane-assets/chunk-ST2Q62ET.js +0 -1
- package/lib/public/pane-assets/style-JVCOCG3W.css +0 -1
package/lib/public/style.css
CHANGED
|
@@ -32,7 +32,7 @@ function attachWorkbenchLayoutSettings(ctx) {
|
|
|
32
32
|
req.on('data', function (chunk) {
|
|
33
33
|
if (finished) return;
|
|
34
34
|
body += chunk;
|
|
35
|
-
if (Buffer.byteLength(body) >
|
|
35
|
+
if (Buffer.byteLength(body) > 65536) { finished = true; body = ''; reply(res, 413, { error: 'Request too large.' }); }
|
|
36
36
|
});
|
|
37
37
|
req.on('aborted', function () { finished = true; });
|
|
38
38
|
req.on('error', function () { finished = true; });
|
|
@@ -41,11 +41,11 @@ function attachWorkbenchLayoutSettings(ctx) {
|
|
|
41
41
|
if (identity(req) !== accountId) { reply(res, 401, { error: 'Account changed. Sign in again.' }); return; }
|
|
42
42
|
var input;
|
|
43
43
|
try { input = JSON.parse(body); } catch (error) { reply(res, 400, { error: 'Invalid JSON.' }); return; }
|
|
44
|
-
if (!input || Array.isArray(input) || Object.keys(input).
|
|
44
|
+
if (!input || Array.isArray(input) || Object.keys(input).some(function (key) { return ['mode', 'revision', 'layout'].indexOf(key) < 0; }) || !Object.prototype.hasOwnProperty.call(input, 'mode') || !Object.prototype.hasOwnProperty.call(input, 'revision')) {
|
|
45
45
|
reply(res, 400, { error: 'Invalid workbench preference.' }); return;
|
|
46
46
|
}
|
|
47
47
|
try {
|
|
48
|
-
var result = ctx.users.setWorkbenchLayoutPreference(accountId, input.mode, input.revision);
|
|
48
|
+
var result = ctx.users.setWorkbenchLayoutPreference(accountId, input.mode, input.revision, input.layout);
|
|
49
49
|
reply(res, result.error ? result.status || 400 : 200, result);
|
|
50
50
|
} catch (error) { reply(res, 500, { error: 'Workbench layout could not be saved. Your previous layout is retained.' }); }
|
|
51
51
|
});
|
package/lib/server.js
CHANGED
|
@@ -317,6 +317,7 @@ function createServer(opts) {
|
|
|
317
317
|
|
|
318
318
|
// --- Project registry ---
|
|
319
319
|
var assignmentService = attachWorkspaceAssignmentService({
|
|
320
|
+
resolveMate: function (owner, id) { return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? owner : null), id); },
|
|
320
321
|
getPresentation: function (principal) {
|
|
321
322
|
var project = projects.get(principal.sourceProjectSlug), manager = project && project.getSessionManager(), status = project && project.getStatus();
|
|
322
323
|
var session = manager && manager.sessions.get(principal.sourceSessionId);
|
package/lib/split-view-layout.js
CHANGED
|
@@ -12,7 +12,7 @@ function validateTree(tree, expectedOrigins) {
|
|
|
12
12
|
var invalid = null;
|
|
13
13
|
function walk(node, depth) {
|
|
14
14
|
if (invalid) return;
|
|
15
|
-
if (!node || typeof node !== "object" || depth > MAX_SESSIONS) { invalid = "Layout tree is malformed"; return; }
|
|
15
|
+
if (!node || typeof node !== "object" || Array.isArray(node) || depth > MAX_SESSIONS) { invalid = "Layout tree is malformed"; return; }
|
|
16
16
|
if (Object.prototype.hasOwnProperty.call(node, "id")) {
|
|
17
17
|
if (Object.keys(node).length !== 1 || !validOrigin(node.id) || origins.indexOf(node.id) !== -1) {
|
|
18
18
|
invalid = "Layout leaves must have unique valid session origins"; return;
|
|
@@ -28,11 +28,11 @@ function validateTree(tree, expectedOrigins) {
|
|
|
28
28
|
}
|
|
29
29
|
walk(node.left, depth + 1); walk(node.right, depth + 1);
|
|
30
30
|
}
|
|
31
|
-
walk(tree, 0);
|
|
31
|
+
if (tree !== null) walk(tree, 0);
|
|
32
32
|
if (invalid) return { ok: false, error: invalid, origins: origins };
|
|
33
|
-
if (origins.length
|
|
33
|
+
if (origins.length > MAX_SESSIONS) return { ok: false, error: "A View supports zero to eight sessions", origins: origins };
|
|
34
34
|
if (expectedOrigins) {
|
|
35
|
-
if (!Array.isArray(expectedOrigins) || expectedOrigins.length !== origins.length ||
|
|
35
|
+
if (!Array.isArray(expectedOrigins) || expectedOrigins.length !== origins.length || new Set(expectedOrigins).size !== expectedOrigins.length ||
|
|
36
36
|
expectedOrigins.some(function (id) { return !validOrigin(id) || origins.indexOf(id) === -1; })) {
|
|
37
37
|
return { ok: false, error: "Layout leaves must exactly match the selected sessions", origins: origins };
|
|
38
38
|
}
|
|
@@ -100,9 +100,9 @@ function validateViewShape(view) {
|
|
|
100
100
|
var name = validateName(view.name);
|
|
101
101
|
if (!name.ok) return name;
|
|
102
102
|
if (view.nameMode != null && ["automatic", "custom"].indexOf(view.nameMode) === -1) return { ok: false, error: "Split view name mode is invalid" };
|
|
103
|
-
if (!Number.
|
|
103
|
+
if (!Number.isSafeInteger(view.revision) || view.revision < 1 || !validatePreset(view.layout)) return { ok: false, error: "Split view revision or layout is invalid" };
|
|
104
104
|
var refs = view.sessionRefs;
|
|
105
|
-
if (!Array.isArray(refs) || refs.length
|
|
105
|
+
if (!Array.isArray(refs) || refs.length > MAX_SESSIONS) return { ok: false, error: "A View supports zero to eight sessions" };
|
|
106
106
|
var ids = [];
|
|
107
107
|
for (var i = 0; i < refs.length; i++) {
|
|
108
108
|
var ref = refs[i];
|
|
@@ -113,7 +113,11 @@ function validateViewShape(view) {
|
|
|
113
113
|
}
|
|
114
114
|
var tree = validateTree(view.tree, ids);
|
|
115
115
|
if (!tree.ok) return tree;
|
|
116
|
-
if (typeof view.activeSessionOriginId !== "string" || ids.indexOf(view.activeSessionOriginId) === -1) return { ok: false, error: "Active session must belong to the Split view" };
|
|
116
|
+
if (ids.length ? typeof view.activeSessionOriginId !== "string" || ids.indexOf(view.activeSessionOriginId) === -1 : view.activeSessionOriginId !== null) return { ok: false, error: "Active session must belong to the Split view" };
|
|
117
|
+
if (view.workspace !== undefined) {
|
|
118
|
+
var checked = require("./project-view-workspace").validate(view.workspace, view);
|
|
119
|
+
if (!checked.ok) return checked;
|
|
120
|
+
}
|
|
117
121
|
return { ok: true, name: name.name, origins: ids };
|
|
118
122
|
}
|
|
119
123
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
var windowLayouts = require('./workbench-window-layout');
|
|
1
2
|
// Account UI preferences are excluded from canonical Clay controls.
|
|
2
3
|
function validMode(mode) { return mode === 'overlay' || mode === 'docked'; }
|
|
3
4
|
function attachWorkbenchLayoutPreferences(deps) {
|
|
@@ -12,16 +13,25 @@ function attachWorkbenchLayoutPreferences(deps) {
|
|
|
12
13
|
function value(accountId, saved) {
|
|
13
14
|
if (saved === undefined) return { accountId: accountId, mode: 'overlay', revision: 0 };
|
|
14
15
|
if (!saved || !validMode(saved.mode) || !Number.isSafeInteger(saved.revision) || saved.revision < 1) throw new Error('Invalid stored preference.');
|
|
15
|
-
|
|
16
|
+
if (saved.layouts && !windowLayouts.validLayouts(saved.layouts)) throw new Error('Invalid stored window layout.');
|
|
17
|
+
return Object.assign({ accountId: accountId, mode: saved.mode, revision: saved.revision }, saved.layouts ? { layouts: saved.layouts } : {});
|
|
16
18
|
}
|
|
17
19
|
function get(accountId) { return value(accountId, context(accountId).saved); }
|
|
18
|
-
function set(accountId, mode, revision) {
|
|
20
|
+
function set(accountId, mode, revision, layout) {
|
|
21
|
+
if (layout !== undefined && !windowLayouts.validLayout(layout)) return { status: 400, error: 'Invalid window layout.' };
|
|
19
22
|
if (!validMode(mode) || !Number.isSafeInteger(revision) || revision < 0) return { status: 400, error: 'Invalid workbench layout.' };
|
|
20
23
|
var current = context(accountId), prior = value(accountId, current.saved);
|
|
21
24
|
if (prior.revision !== revision) return { status: 409, error: 'Workbench layout changed elsewhere. Reload the preference and try again.' };
|
|
22
|
-
|
|
25
|
+
var layouts = Object.assign({}, prior.layouts || {});
|
|
26
|
+
var changed = layout && JSON.stringify(layouts[layout.key]) !== JSON.stringify({ base: layout.base, tree: layout.tree });
|
|
27
|
+
if (prior.mode === mode && !changed) return prior;
|
|
28
|
+
if (layout) {
|
|
29
|
+
delete layouts[layout.key];
|
|
30
|
+
if (Object.keys(layouts).length >= 32) delete layouts[Object.keys(layouts)[0]];
|
|
31
|
+
layouts[layout.key] = { base: layout.base, tree: layout.tree };
|
|
32
|
+
}
|
|
23
33
|
if (revision === Number.MAX_SAFE_INTEGER) throw new Error('Preference revision exhausted.');
|
|
24
|
-
var saved = { mode: mode, revision: revision + 1 };
|
|
34
|
+
var saved = Object.assign({ mode: mode, revision: revision + 1 }, Object.keys(layouts).length ? { layouts: layouts } : {});
|
|
25
35
|
if (current.settings) config.saveConfig(Object.assign({}, current.settings, { workbenchLayoutPreference: saved }));
|
|
26
36
|
else deps.saveUsers(Object.assign({}, current.data, { users: current.data.users.map(function (entry) {
|
|
27
37
|
return entry === current.user ? Object.assign({}, entry, { workbenchLayoutPreference: saved }) : entry;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Display preferences only: these records never open sessions or run tools.
|
|
2
|
+
var tools = ['tool-terminal', 'tool-browser', 'tool-file-viewer'];
|
|
3
|
+
function validTree(tree, allowTools) {
|
|
4
|
+
var ids = [], nodes = 0;
|
|
5
|
+
function walk(node, depth) {
|
|
6
|
+
if (!node || typeof node !== 'object' || Array.isArray(node) || ++nodes > 21 || depth > 10) return false;
|
|
7
|
+
if (Object.prototype.hasOwnProperty.call(node, 'id')) {
|
|
8
|
+
if (Object.keys(node).length !== 1 || typeof node.id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(node.id)) return false;
|
|
9
|
+
if (node.id.indexOf('tool-') === 0 && (!allowTools || tools.indexOf(node.id) < 0)) return false;
|
|
10
|
+
ids.push(node.id); return true;
|
|
11
|
+
}
|
|
12
|
+
return Object.keys(node).sort().join(',') === 'axis,left,ratio,right' && ['horizontal', 'vertical'].indexOf(node.axis) >= 0 &&
|
|
13
|
+
Number.isFinite(node.ratio) && node.ratio >= .05 && node.ratio <= .95 && walk(node.left, depth + 1) && walk(node.right, depth + 1);
|
|
14
|
+
}
|
|
15
|
+
return walk(tree, 0) && ids.length > 0 && ids.length <= (allowTools ? 11 : 8) && new Set(ids).size === ids.length;
|
|
16
|
+
}
|
|
17
|
+
function validLayout(input) {
|
|
18
|
+
return !!input && typeof input === 'object' && !Array.isArray(input) && Object.keys(input).sort().join(',') === 'base,key,tree' &&
|
|
19
|
+
typeof input.key === 'string' && input.key.length > 0 && input.key.length <= 512 && input.key !== '__proto__' &&
|
|
20
|
+
validTree(input.base, false) && validTree(input.tree, true) && sameSessions(input.base, input.tree);
|
|
21
|
+
}
|
|
22
|
+
function sameSessions(base, tree) {
|
|
23
|
+
function ids(node) { return node.id ? (tools.indexOf(node.id) >= 0 ? [] : [node.id]) : ids(node.left).concat(ids(node.right)); }
|
|
24
|
+
return JSON.stringify(ids(base).sort()) === JSON.stringify(ids(tree).sort());
|
|
25
|
+
}
|
|
26
|
+
function validLayouts(layouts) {
|
|
27
|
+
return !!layouts && typeof layouts === 'object' && !Array.isArray(layouts) && Object.keys(layouts).length <= 32 &&
|
|
28
|
+
Object.keys(layouts).every(function (key) { var item = layouts[key]; return item && validLayout({ key: key, base: item.base, tree: item.tree }); });
|
|
29
|
+
}
|
|
30
|
+
module.exports = { validLayout: validLayout, validLayouts: validLayouts };
|
|
@@ -2,7 +2,7 @@ var fs = require("fs");
|
|
|
2
2
|
var path = require("path");
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
|
|
5
|
-
var ACTIVE = { proposed: true, starting: true, running: true };
|
|
5
|
+
var ACTIVE = { proposed: true, queued: true, starting: true, running: true };
|
|
6
6
|
var MAX_RUNNING = 3;
|
|
7
7
|
|
|
8
8
|
function clean(value, max) {
|
|
@@ -21,6 +21,8 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
21
21
|
var pendingRestartReplay = {};
|
|
22
22
|
var presentations = new WeakMap();
|
|
23
23
|
var presentationTargets = new WeakMap();
|
|
24
|
+
var followUps = require('./workspace-follow-up-queue').createFollowUpQueue({ records: records,
|
|
25
|
+
exactSource: exactSource, exactTarget: exactTarget, settle: settle, approve: approve });
|
|
24
26
|
|
|
25
27
|
function recordFile(userId) {
|
|
26
28
|
return storageDir ? path.join(storageDir, userStorageKey(userId)) : null;
|
|
@@ -44,12 +46,13 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
44
46
|
if (!files[i].endsWith(".json") || files[i].indexOf(".tmp.") !== -1) continue;
|
|
45
47
|
try {
|
|
46
48
|
var loaded = JSON.parse(fs.readFileSync(path.join(storageDir, files[i]), "utf8"));
|
|
47
|
-
if (Array.isArray(loaded)) records
|
|
49
|
+
if (Array.isArray(loaded)) records.push.apply(records, loaded);
|
|
48
50
|
} catch (e) {}
|
|
49
51
|
}
|
|
50
52
|
var changedUsers = {};
|
|
51
53
|
for (var j = 0; j < records.length; j++) {
|
|
52
|
-
if (records[j].status !== "starting" && records[j].status !== "running") continue;
|
|
54
|
+
if (records[j].status !== "queued" && records[j].status !== "starting" && records[j].status !== "running") continue;
|
|
55
|
+
records[j].deliveryState = records[j].status === "queued" ? "not_sent" : "unknown";
|
|
53
56
|
records[j].status = "interrupted";
|
|
54
57
|
records[j].error = "Clay restarted before this assignment completed.";
|
|
55
58
|
records[j].updatedAt = Date.now();
|
|
@@ -90,6 +93,8 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
90
93
|
title: record.title,
|
|
91
94
|
task: record.task,
|
|
92
95
|
status: record.status,
|
|
96
|
+
deliveryState: record.deliveryState || null,
|
|
97
|
+
parentAssignmentId: record.parentAssignmentId || null,
|
|
93
98
|
sourceProjectSlug: record.sourceProjectSlug,
|
|
94
99
|
sourceSessionRef: record.sourceSessionRef,
|
|
95
100
|
targetSessionRef: record.targetSessionRef || null,
|
|
@@ -119,6 +124,8 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
119
124
|
var status = project.getStatus();
|
|
120
125
|
if (!status || status.isMate !== true || status.mateId !== record.sourceMateId) return null;
|
|
121
126
|
if (ctx.isMultiUser() && status.projectOwnerId !== record.userId) return null;
|
|
127
|
+
var mate = ctx.resolveMate && ctx.resolveMate(status.projectOwnerId || null, record.sourceMateId);
|
|
128
|
+
if (ctx.resolveMate && (!mate || mate.id !== record.sourceMateId || ctx.isMultiUser() && mate.createdBy !== record.userId)) return null;
|
|
122
129
|
var manager = project.getSessionManager();
|
|
123
130
|
var session = null;
|
|
124
131
|
if (manager && manager.sessions) {
|
|
@@ -129,7 +136,7 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
129
136
|
if (!session) return null;
|
|
130
137
|
if (ctx.isMultiUser() && session.ownerId !== record.userId) return null;
|
|
131
138
|
if (!ctx.isMultiUser() && session.ownerId && session.ownerId !== record.singleUserOwnerId) return null;
|
|
132
|
-
return { project: project, manager: manager, session: session };
|
|
139
|
+
return { project: project, manager: manager, session: session, mate: mate || null };
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
function present(record, principal) {
|
|
@@ -149,7 +156,7 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
149
156
|
function emit(record, eventType) {
|
|
150
157
|
var observer = presentations.get(record);
|
|
151
158
|
if (observer) {
|
|
152
|
-
if (record.status === 'starting') observer.phase('queued');
|
|
159
|
+
if (record.status === 'starting' || record.status === 'queued') observer.phase('queued');
|
|
153
160
|
else if (record.status === 'running') observer.phase('running');
|
|
154
161
|
else if (record.status !== 'proposed') { observer.finish({ status: record.status === 'interrupted' ? 'failed' : record.status, outcome: record.status === 'interrupted' ? 'uncertain' : undefined }); presentations.delete(record); }
|
|
155
162
|
}
|
|
@@ -232,12 +239,16 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
232
239
|
var idempotency = crypto.createHash("sha256").update(key + "\u0000follow_up\u0000" + projectSlug + "\u0000" + targetSessionRef + "\u0000" + title + "\u0000" + task).digest("hex");
|
|
233
240
|
for (var i = 0; i < records.length; i++) {
|
|
234
241
|
if (records[i].idempotencyKey === idempotency) return publicRecord(records[i]);
|
|
235
|
-
if (records[i].sourceKey === key && ACTIVE[records[i].status]
|
|
242
|
+
if (records[i].sourceKey === key && ACTIVE[records[i].status] &&
|
|
243
|
+
(records[i].targetProjectSlug !== projectSlug || records[i].targetSessionRef !== targetSessionRef)) throw new Error("An active assignment already exists. Send this update to that exact Driver session.");
|
|
236
244
|
}
|
|
237
245
|
var candidate = { userId: principal.userId || null, singleUserOwnerId: principal.singleUserOwnerId || null, targetProjectSlug: projectSlug };
|
|
238
246
|
var target = exactTarget(candidate);
|
|
239
247
|
if (!target || typeof target.inspectDelegatedFollowUp !== "function") throw new Error("Target project is unavailable for follow-ups.");
|
|
240
|
-
|
|
248
|
+
if (records.filter(function (item) { return item.sourceKey === key && item.delivery === 'follow_up' && ACTIVE[item.status]; }).length >= 16) throw new Error('There are already 16 pending follow-ups. Wait for or cancel a review.');
|
|
249
|
+
var candidateInput = { userId: principal.userId || null, sourceKey: key, targetProjectSlug: projectSlug, targetSessionRef: targetSessionRef,
|
|
250
|
+
sourceMateId: principal.mateId, sourceProjectSlug: principal.sourceProjectSlug, sourceSessionRef: principal.sourceSessionRef };
|
|
251
|
+
var inspected = followUps.inspect(candidateInput, target);
|
|
241
252
|
var now = Date.now();
|
|
242
253
|
var targetStatus = target.getStatus();
|
|
243
254
|
var record = {
|
|
@@ -250,6 +261,7 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
250
261
|
title: title, task: task, delivery: "follow_up", status: "proposed", createdAt: now, updatedAt: now,
|
|
251
262
|
};
|
|
252
263
|
if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
|
|
264
|
+
followUps.remember(record, target, inspected);
|
|
253
265
|
records.push(record);
|
|
254
266
|
saveUser(record.userId);
|
|
255
267
|
present(record, principal);
|
|
@@ -279,7 +291,9 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
279
291
|
|
|
280
292
|
function settle(record, status, session, error) {
|
|
281
293
|
if (!ACTIVE[record.status]) return publicRecord(record);
|
|
294
|
+
if (record.delivery === "follow_up" && record.deliveryState !== "accepted") record.deliveryState = record.status === "starting" ? "unknown" : "not_sent";
|
|
282
295
|
record.status = status;
|
|
296
|
+
followUps.cleanup(record);
|
|
283
297
|
record.resultSummary = status === "completed" ? summarize(session) : "";
|
|
284
298
|
record.error = clean(error || "", 800);
|
|
285
299
|
record.updatedAt = Date.now();
|
|
@@ -287,12 +301,19 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
287
301
|
delete unsubscribers[record.assignmentId];
|
|
288
302
|
saveUser(record.userId);
|
|
289
303
|
emit(record);
|
|
304
|
+
followUps.wake(record);
|
|
290
305
|
return publicRecord(record);
|
|
291
306
|
}
|
|
292
307
|
|
|
293
308
|
function watch(record, manager, session) {
|
|
294
309
|
var view = presentationTargets.get(record); if (view) { view.session = session; view.origin = session.sessionOriginId; var observer = presentations.get(record); if (observer && observer.target) observer.target({ kind: 'session', projectSlug: record.targetProjectSlug, sessionId: session.localId, sessionOriginId: session.sessionOriginId }); }
|
|
310
|
+
if (unsubscribers[record.assignmentId]) unsubscribers[record.assignmentId]();
|
|
295
311
|
var unsubscribe = manager.subscribeSession(session.localId, function (event) {
|
|
312
|
+
if (record.status === 'queued') {
|
|
313
|
+
if (event.type === 'error') settle(record, 'failed', session, event.text || 'Driver failed before follow-up delivery.');
|
|
314
|
+
else if (event.type === 'done' || event.type === 'result') followUps.schedule(record);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
296
317
|
if (event.type === "session_id") {
|
|
297
318
|
record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
|
|
298
319
|
record.updatedAt = Date.now();
|
|
@@ -301,6 +322,7 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
301
322
|
} else if (event.type === "error") {
|
|
302
323
|
settle(record, "failed", session, event.text || "Assignment failed.");
|
|
303
324
|
} else if (event.type === "done" || event.type === "result") {
|
|
325
|
+
if (record.delivery === "follow_up") record.deliveryState = "accepted";
|
|
304
326
|
settle(record, session._lastTurnInterrupted ? "interrupted" : "completed", session, "");
|
|
305
327
|
}
|
|
306
328
|
});
|
|
@@ -308,17 +330,32 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
308
330
|
}
|
|
309
331
|
|
|
310
332
|
async function approve(record) {
|
|
311
|
-
if (record.status !== "proposed") return publicRecord(record);
|
|
333
|
+
if (record.status !== "proposed" && record.status !== "queued") return publicRecord(record);
|
|
312
334
|
var running = records.filter(function (item) {
|
|
313
335
|
return item.userId === record.userId && (item.status === "starting" || item.status === "running");
|
|
314
336
|
}).length;
|
|
315
|
-
if (running >= MAX_RUNNING) throw new Error("Too many assignments are already running.");
|
|
316
337
|
if (!exactSource(record)) throw new Error("Source conversation is no longer available.");
|
|
317
338
|
var target = exactTarget(record);
|
|
318
339
|
var followUp = record.delivery === "follow_up";
|
|
319
340
|
if (!target || (followUp ? typeof target.dispatchDelegatedFollowUp !== "function" : typeof target.createDelegatedSession !== "function")) throw new Error("Target project is no longer available.");
|
|
341
|
+
if (followUp) {
|
|
342
|
+
var inspected;
|
|
343
|
+
try { inspected = followUps.validate(record, target, followUps.inspect(record, target)); }
|
|
344
|
+
catch (error) { return settle(record, 'failed', null, error.message); }
|
|
345
|
+
if (followUps.busy(inspected.session) || (followUps.first(record) && followUps.first(record) !== record)) {
|
|
346
|
+
var beforeQueue = Object.assign({}, record, { decision: record.decision, deliveryState: record.deliveryState, queueOrder: record.queueOrder });
|
|
347
|
+
if (record.status === 'proposed') record.queueOrder = records.reduce(function (max, item) { return Math.max(max, item.queueOrder || 0); }, 0) + 1;
|
|
348
|
+
record.decision = 'approve'; record.status = 'queued'; record.deliveryState = 'queued'; record.updatedAt = Date.now();
|
|
349
|
+
try { saveUser(record.userId); } catch (error) { Object.assign(record, beforeQueue); throw error; }
|
|
350
|
+
watch(record, target.getSessionManager(), inspected.session); followUps.track(record); emit(record); followUps.schedule(record);
|
|
351
|
+
return publicRecord(record);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (running >= MAX_RUNNING) throw new Error("Too many assignments are already running.");
|
|
355
|
+
followUps.cleanup(record);
|
|
320
356
|
record.decision = "approve";
|
|
321
357
|
record.status = "starting";
|
|
358
|
+
if (followUp) record.deliveryState = "dispatching";
|
|
322
359
|
record.updatedAt = Date.now();
|
|
323
360
|
saveUser(record.userId);
|
|
324
361
|
emit(record);
|
|
@@ -329,10 +366,11 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
329
366
|
sourceMateId: record.sourceMateId,
|
|
330
367
|
sourceProjectSlug: record.sourceProjectSlug,
|
|
331
368
|
sourceSessionRef: record.sourceSessionRef,
|
|
369
|
+
parentAssignmentId: record.parentAssignmentId || null,
|
|
332
370
|
};
|
|
333
371
|
var session;
|
|
334
372
|
if (followUp) {
|
|
335
|
-
session = await target.dispatchDelegatedFollowUp({ userId: record.userId }, record.targetSessionRef, record.task, metadata, function (existing) { watch(record, target.getSessionManager(), existing); });
|
|
373
|
+
session = await target.dispatchDelegatedFollowUp({ userId: record.userId, sourceMateId: record.sourceMateId, sourceProjectSlug: record.sourceProjectSlug, sourceSessionRef: record.sourceSessionRef }, record.targetSessionRef, record.task, metadata, function (existing) { followUps.validate(record, exactTarget(record), followUps.inspect(record, target)); watch(record, target.getSessionManager(), existing); });
|
|
336
374
|
} else {
|
|
337
375
|
session = await target.createDelegatedSession({ userId: record.userId }, record.task, metadata, function (created) { watch(record, target.getSessionManager(), created); });
|
|
338
376
|
}
|
|
@@ -340,6 +378,7 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
340
378
|
record.targetSessionRef = ctx.sessionRef(record.targetProjectSlug, session);
|
|
341
379
|
record.targetSessionId = session.localId;
|
|
342
380
|
record.status = "running";
|
|
381
|
+
if (followUp) record.deliveryState = "accepted";
|
|
343
382
|
record.updatedAt = Date.now();
|
|
344
383
|
saveUser(record.userId);
|
|
345
384
|
emit(record);
|
|
@@ -350,12 +389,17 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
350
389
|
}
|
|
351
390
|
|
|
352
391
|
function cancel(record) {
|
|
353
|
-
if (record.status !== "proposed") return publicRecord(record);
|
|
392
|
+
if (record.status !== "proposed" && record.status !== "queued") return publicRecord(record);
|
|
393
|
+
followUps.cleanup(record);
|
|
394
|
+
if (unsubscribers[record.assignmentId]) unsubscribers[record.assignmentId]();
|
|
395
|
+
delete unsubscribers[record.assignmentId];
|
|
396
|
+
record.deliveryState = 'not_sent';
|
|
354
397
|
record.decision = "cancel";
|
|
355
398
|
record.status = "cancelled";
|
|
356
399
|
record.updatedAt = Date.now();
|
|
357
400
|
saveUser(record.userId);
|
|
358
401
|
emit(record);
|
|
402
|
+
followUps.wake(record);
|
|
359
403
|
return publicRecord(record);
|
|
360
404
|
}
|
|
361
405
|
|
|
@@ -396,6 +440,7 @@ function attachWorkspaceAssignmentService(ctx) {
|
|
|
396
440
|
validateResponseSource(ws, msg, record, routedProjectSlug);
|
|
397
441
|
var action = msg.action === "approve" ? "approve" : (msg.action === "cancel" ? "cancel" : "");
|
|
398
442
|
if (!action) throw new Error("Choose approve or cancel.");
|
|
443
|
+
if (record.status === "queued" && action === "cancel") return cancel(record);
|
|
399
444
|
if (record.status !== "proposed") {
|
|
400
445
|
if (record.decision === action) return publicRecord(record);
|
|
401
446
|
throw new Error("Assignment proposal has already been resolved.");
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Reviewed assignment updates share the existing follow-up dispatch service.
|
|
2
|
+
function createFollowUpQueue(ctx) {
|
|
3
|
+
var snapshots = new WeakMap(), scheduled = new WeakSet(), sourceWatches = new WeakMap();
|
|
4
|
+
function source(record) {
|
|
5
|
+
var current = ctx.exactSource(record);
|
|
6
|
+
if (!current || current.session.taskStopRequested || current.session._pendingMessageDrainPaused || current.session._lastTurnInterrupted || current.session.abortController && current.session.abortController.signal.aborted || current.session._mentionInProgress || current.session.spawn) throw new Error('Source conversation stopped or changed. Request a new review.');
|
|
7
|
+
return current;
|
|
8
|
+
}
|
|
9
|
+
function inspect(record, project) {
|
|
10
|
+
var parent = ctx.records.find(function (item) {
|
|
11
|
+
return item.sourceKey === record.sourceKey && item.delivery !== 'follow_up' && item.targetProjectSlug === record.targetProjectSlug &&
|
|
12
|
+
item.targetSessionRef === record.targetSessionRef && (!record.parentAssignmentId || item.assignmentId === record.parentAssignmentId);
|
|
13
|
+
});
|
|
14
|
+
return project.inspectDelegatedFollowUp({ userId: record.userId, sourceMateId: record.sourceMateId,
|
|
15
|
+
sourceProjectSlug: record.sourceProjectSlug, sourceSessionRef: record.sourceSessionRef }, record.targetSessionRef,
|
|
16
|
+
{ allowBusy: true, parentAssignmentId: parent && parent.assignmentId || null });
|
|
17
|
+
}
|
|
18
|
+
function remember(record, project, inspected) {
|
|
19
|
+
var current = source(record);
|
|
20
|
+
snapshots.set(record, { project: project, session: inspected.session, origin: inspected.session.sessionOriginId,
|
|
21
|
+
linuxUser: inspected.linuxUser || null, source: current.session, sourceProject: current.project });
|
|
22
|
+
record.targetSessionOriginId = inspected.session.sessionOriginId || null;
|
|
23
|
+
record.targetLinuxUser = inspected.linuxUser || null;
|
|
24
|
+
record.targetDirectory = project.getStatus().path || null;
|
|
25
|
+
record.sourceDirectory = current.project.getStatus().path || null;
|
|
26
|
+
record.sourceVisibility = current.session.sessionVisibility || null;
|
|
27
|
+
record.sourceBuiltinKey = current.mate && current.mate.builtinKey || null;
|
|
28
|
+
record.sourceSessionOriginId = current.session.sessionOriginId || null;
|
|
29
|
+
record.sourceQueryGeneration = current.session._sdkQueryGeneration || null;
|
|
30
|
+
var parent = ctx.records.find(function (item) { return item.sourceKey === record.sourceKey && item.delivery !== 'follow_up' &&
|
|
31
|
+
item.targetProjectSlug === record.targetProjectSlug && item.targetSessionRef === record.targetSessionRef; });
|
|
32
|
+
record.parentAssignmentId = parent && parent.assignmentId || null;
|
|
33
|
+
}
|
|
34
|
+
function validate(record, project, inspected) {
|
|
35
|
+
var current = source(record), saved = snapshots.get(record);
|
|
36
|
+
if (saved && (saved.project !== project || saved.session !== inspected.session || saved.origin !== inspected.session.sessionOriginId ||
|
|
37
|
+
saved.linuxUser !== (inspected.linuxUser || null) || saved.source !== current.session || saved.sourceProject !== current.project) ||
|
|
38
|
+
record.targetSessionOriginId && record.targetSessionOriginId !== inspected.session.sessionOriginId ||
|
|
39
|
+
record.sourceSessionOriginId && record.sourceSessionOriginId !== current.session.sessionOriginId ||
|
|
40
|
+
record.parentAssignmentId && (!inspected.session.assignment || inspected.session.assignment.assignmentId !== record.parentAssignmentId) ||
|
|
41
|
+
record.sourceVisibility !== (current.session.sessionVisibility || null) ||
|
|
42
|
+
record.targetDirectory !== (project.getStatus().path || null) || record.sourceDirectory !== (current.project.getStatus().path || null) ||
|
|
43
|
+
record.sourceBuiltinKey !== (current.mate && current.mate.builtinKey || null) ||
|
|
44
|
+
(record.targetLinuxUser || null) !== (inspected.linuxUser || null) ||
|
|
45
|
+
(record.sourceQueryGeneration || null) !== (current.session._sdkQueryGeneration || null)) throw new Error('Follow-up identity or OS mapping changed. Request a new review.');
|
|
46
|
+
return inspected;
|
|
47
|
+
}
|
|
48
|
+
function busy(session) { return !!(session.isProcessing || session._queryStarting || session._awaitingTurnResult); }
|
|
49
|
+
function cleanup(record) {
|
|
50
|
+
var unsubscribe = sourceWatches.get(record);
|
|
51
|
+
if (unsubscribe) unsubscribe();
|
|
52
|
+
sourceWatches.delete(record);
|
|
53
|
+
}
|
|
54
|
+
function track(record) {
|
|
55
|
+
if (sourceWatches.has(record)) return;
|
|
56
|
+
var current = source(record);
|
|
57
|
+
if (typeof current.manager.subscribeSession !== 'function') return;
|
|
58
|
+
var unsubscribe = current.manager.subscribeSession(current.session.localId, function () {
|
|
59
|
+
if (record.status !== 'queued') return;
|
|
60
|
+
try { validate(record, ctx.exactTarget(record), inspect(record, ctx.exactTarget(record))); } catch (error) { stop(record, 'interrupted', error); }
|
|
61
|
+
});
|
|
62
|
+
if (unsubscribe) sourceWatches.set(record, unsubscribe);
|
|
63
|
+
}
|
|
64
|
+
function first(record) {
|
|
65
|
+
return ctx.records.filter(function (item) { return item.status === 'queued' && item.userId === record.userId &&
|
|
66
|
+
item.targetProjectSlug === record.targetProjectSlug && item.targetSessionRef === record.targetSessionRef; })
|
|
67
|
+
.sort(function (a, b) { return a.queueOrder - b.queueOrder; })[0];
|
|
68
|
+
}
|
|
69
|
+
function wake(record) { var next = first(record); if (next) schedule(next); }
|
|
70
|
+
function stop(record, status, error) {
|
|
71
|
+
try { ctx.settle(record, status, null, error.message); }
|
|
72
|
+
catch (failure) { console.error('Follow-up status could not be persisted:', failure.message); }
|
|
73
|
+
}
|
|
74
|
+
function schedule(record) {
|
|
75
|
+
if (record.status !== 'queued' || scheduled.has(record)) return;
|
|
76
|
+
scheduled.add(record);
|
|
77
|
+
setImmediate(function () {
|
|
78
|
+
scheduled.delete(record);
|
|
79
|
+
if (record.status !== 'queued') return;
|
|
80
|
+
try {
|
|
81
|
+
var project = ctx.exactTarget(record);
|
|
82
|
+
if (!project) throw new Error('Target project access changed.');
|
|
83
|
+
var inspected = validate(record, project, inspect(record, project));
|
|
84
|
+
if (inspected.session._lastTurnInterrupted) throw new Error('Driver was stopped; explicit owner resumption is required.');
|
|
85
|
+
if (first(record) !== record || busy(inspected.session)) return;
|
|
86
|
+
Promise.resolve(ctx.approve(record)).catch(function (error) { stop(record, 'failed', error); });
|
|
87
|
+
} catch (error) { stop(record, 'interrupted', error); }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { inspect: inspect, remember: remember, validate: validate, busy: busy, track: track, schedule: schedule, first: first, wake: wake, cleanup: cleanup };
|
|
91
|
+
}
|
|
92
|
+
module.exports = { createFollowUpQueue: createFollowUpQueue };
|
|
@@ -90,7 +90,7 @@ function commonTools(bound) {
|
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
92
|
name: "propose_project_follow_up",
|
|
93
|
-
description: "Propose
|
|
93
|
+
description: "Propose reviewed instructions for an exact existing private Driver session, including an update to your active assignment. Busy Drivers queue approved instructions for their next turn; stopped sessions require explicit owner resumption. No target work occurs before human approval.",
|
|
94
94
|
inputSchema: shape({
|
|
95
95
|
projectSlug: field("string", "Exact owned target project slug."),
|
|
96
96
|
targetSessionRef: field("string", "Durable opaque session reference returned by list_project_sessions."),
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{l as a,m as b,n as c,o as d}from"./chunk-IZ2ZANEY.js";import"./chunk-7H36SUZA.js";import"./chunk-DGOFGEUK.js";export{a as closeLinear,c as handleLinearResult,d as initLinearPanel,b as openLinear};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{K as a,L as b,M as c,N as d,O as e,P as f}from"./chunk-7H36SUZA.js";import"./chunk-DGOFGEUK.js";export{e as handleSharedBrowserMessage,d as hideSharedBrowser,f as initSharedBrowser,c as openSharedBrowser,b as openSharedBrowserAt,a as sendBrowserInput};
|