clay-server 4.8.0-beta.1 → 4.8.0-beta.3

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.
Files changed (60) hide show
  1. package/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
  2. package/lib/clay-skill-install.js +33 -0
  3. package/lib/clay-skill-storage.js +49 -0
  4. package/lib/linear-api.js +62 -0
  5. package/lib/linear-store.js +52 -0
  6. package/lib/project-http.js +14 -5
  7. package/lib/project-linear-launch.js +70 -0
  8. package/lib/project-session-linear.js +197 -0
  9. package/lib/project-shared-browser.js +256 -0
  10. package/lib/project.js +74 -2
  11. package/lib/public/app.js +7 -0
  12. package/lib/public/css/filebrowser.css +2 -25
  13. package/lib/public/css/issues.css +1 -14
  14. package/lib/public/css/linear.css +35 -0
  15. package/lib/public/css/mcp-skills-workbench.css +51 -0
  16. package/lib/public/css/project-logs.css +0 -11
  17. package/lib/public/css/right-workbench.css +28 -0
  18. package/lib/public/css/scheduled-tasks.css +1 -1
  19. package/lib/public/css/settings-ui.css +295 -0
  20. package/lib/public/css/shared-browser-skins.css +28 -0
  21. package/lib/public/css/shared-browser.css +91 -0
  22. package/lib/public/css/sticky-notes.css +0 -11
  23. package/lib/public/index.html +50 -22
  24. package/lib/public/modules/app-messages.js +6 -0
  25. package/lib/public/modules/app-projects.js +1 -1
  26. package/lib/public/modules/extension-settings.js +110 -0
  27. package/lib/public/modules/issues.js +4 -1
  28. package/lib/public/modules/linear-panel.js +120 -0
  29. package/lib/public/modules/linear-settings.js +61 -0
  30. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  31. package/lib/public/modules/mcp-ui.js +9 -12
  32. package/lib/public/modules/notifications.js +2 -107
  33. package/lib/public/modules/project-settings.js +19 -1
  34. package/lib/public/modules/session-github.js +15 -5
  35. package/lib/public/modules/shared-browser-activity.js +66 -0
  36. package/lib/public/modules/shared-browser-input.js +73 -0
  37. package/lib/public/modules/shared-browser-tabs.js +33 -0
  38. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  39. package/lib/public/modules/shared-browser.js +214 -0
  40. package/lib/public/modules/sidebar-mobile.js +3 -3
  41. package/lib/public/modules/skills-actions.js +247 -0
  42. package/lib/public/modules/skills-detail.js +148 -0
  43. package/lib/public/modules/skills-state.js +10 -0
  44. package/lib/public/modules/skills.js +64 -459
  45. package/lib/public/modules/sticky-notes.js +23 -6
  46. package/lib/public/modules/tool-palette-order.js +11 -13
  47. package/lib/public/modules/user-settings.js +4 -0
  48. package/lib/public/style.css +8 -0
  49. package/lib/sdk-bridge.js +22 -1
  50. package/lib/server-linear.js +50 -0
  51. package/lib/server.js +5 -0
  52. package/lib/sessions.js +7 -0
  53. package/lib/shared-browser-activity-history.js +17 -0
  54. package/lib/shared-browser-policy.js +26 -0
  55. package/lib/shared-browser-runtime.js +82 -0
  56. package/lib/shared-browser-tools.js +21 -0
  57. package/lib/shared-browser-worker.js +160 -0
  58. package/lib/ws-schema.js +10 -0
  59. package/lib/yoke/skill-discovery.js +22 -2
  60. package/package.json +1 -1
@@ -0,0 +1,256 @@
1
+ var activityHistory = require("./shared-browser-activity-history");
2
+ var crypto = require("crypto");
3
+ var policy = require("./shared-browser-policy");
4
+ var browserTools = require("./shared-browser-tools");
5
+ var createRuntime = require("./shared-browser-runtime").createRuntime;
6
+
7
+ function attachSharedBrowser(ctx) {
8
+ var records = new Map();
9
+ var viewers = new Map();
10
+ var destroyed = false;
11
+ function allowed(session, ws) {
12
+ try {
13
+ if (ctx.isMate || !session || ctx.sm.sessions.get(session.localId) !== session) return false;
14
+ if (ctx.usersModule.isMultiUser()) {
15
+ var user = ctx.usersModule.findUserById(session.ownerId);
16
+ if (!user || (ws && (!ws._clayUser || String(ws._clayUser.id) !== String(user.id)))) return false;
17
+ var actor = { _clayUser: user };
18
+ if (!ctx.requestAccess.canAccessProject(actor, ctx.slug) || !ctx.requestAccess.hasPermission(actor, "terminal")) return false;
19
+ } else if (ws && !ctx.requestAccess.hasPermission(ws, "terminal")) return false;
20
+ if (ctx.osUsers && !ctx.getIdentity(session)) return false;
21
+ return true;
22
+ } catch (e) { return false; }
23
+ }
24
+ function identityKey(session) {
25
+ var identity = ctx.getIdentity(session);
26
+ return identity ? JSON.stringify([identity.uid, identity.gid, identity.home]) : "default";
27
+ }
28
+ function recordAllowed(record, ws) {
29
+ try { return allowed(record.session, ws) && record.ownerId === record.session.ownerId && record.identity === identityKey(record.session); }
30
+ catch (e) { return false; }
31
+ }
32
+ function requireAccess(session, ws) {
33
+ if (!allowed(session, ws)) throw new Error("Shared browser access is not permitted for this session");
34
+ }
35
+ function projection(record) {
36
+ if (!record) return null;
37
+ return { id: record.id, phase: record.phase, control: record.control, epoch: record.epoch, handoff: !!record.handoff,
38
+ url: record.url, width: record.width, height: record.height, activity: record.activity || null, activityHistory: record.activityHistory || [], pointer: record.pointer || null, error: record.error || "" };
39
+ }
40
+ function sessionRecords(session) {
41
+ return Array.from(records.values()).filter(function (record) { return record.session === session && recordAllowed(record); });
42
+ }
43
+ function selectRecord(session, id) {
44
+ var tabs = sessionRecords(session);
45
+ if (id) {
46
+ var record = records.get(id);
47
+ if (!record || record.session !== session || !recordAllowed(record)) throw new Error("Browser tab expired");
48
+ return record;
49
+ }
50
+ if (tabs.length > 1) throw new Error("Choose a browserId from status before operating a tab");
51
+ return tabs[0] || null;
52
+ }
53
+ function sendState(ws, session, record, present, focus) {
54
+ if (!ws || ws.readyState !== 1 || !allowed(session, ws) || (record && !recordAllowed(record, ws))) return;
55
+ ctx.sendTo(ws, { type: "shared_browser_state", sessionId: session.localId, browser: projection(record), browsers: sessionRecords(session).map(projection), present: !!present, focus: !!focus });
56
+ }
57
+ function broadcast(record, present) {
58
+ ctx.clients.forEach(function (ws) {
59
+ if (ctx.getSessionForWs(ws) === record.session) sendState(ws, record.session, record, present);
60
+ });
61
+ }
62
+ function watching(record, ws) {
63
+ return ws.readyState === 1 && viewers.get(ws) === record.id && ctx.getSessionForWs(ws) === record.session && recordAllowed(record, ws);
64
+ }
65
+ function visibility(record) {
66
+ if (!record.runtime) return;
67
+ var visible = false;
68
+ viewers.forEach(function (id, ws) { if (id === record.id && watching(record, ws)) visible = true; });
69
+ record.runtime.viewing(visible);
70
+ }
71
+ function end(record, reason) {
72
+ if (record.runtime) record.runtime.close();
73
+ record.runtime = null;
74
+ record.phase = "ended";
75
+ record.error = reason || "";
76
+ record.control = "user";
77
+ activityHistory.interruptActivity(record); record.pointer = null;
78
+ record.epoch++;
79
+ record.rejectReady(new Error(reason || "Browser ended"));
80
+ broadcast(record);
81
+ }
82
+ function receive(record, message) {
83
+ if (destroyed || records.get(record.id) !== record || !record.runtime) return;
84
+ if (!recordAllowed(record)) { end(record, "Browser access was revoked"); records.delete(record.id); return; }
85
+ if (message.type === "activity" || message.type === "pointer") {
86
+ if (message.epoch !== record.epoch || message.generation !== Number(record.session._sdkQueryGeneration || 0) || record.control !== "agent" || record.handoff) return;
87
+ record.activityGeneration = message.generation;
88
+ if (message.type === "activity") activityHistory.recordActivity(record, message.activity);
89
+ else record.pointer = message.pointer;
90
+ broadcast(record); return;
91
+ }
92
+ if ((record.activity || record.pointer) && record.activityGeneration !== Number(record.session._sdkQueryGeneration || 0)) {
93
+ activityHistory.interruptActivity(record); record.pointer = null; broadcast(record);
94
+ }
95
+ if (message.type === "ready") { record.phase = "live"; record.resolveReady(); }
96
+ if (message.type === "error") { end(record, message.error); return; }
97
+ if (message.url !== undefined) record.url = policy.visibleUrl(message.url);
98
+ if (message.width) { record.width = message.width; record.height = message.height; }
99
+ if (message.type === "frame") {
100
+ record.frame = { type: "shared_browser_frame", sessionId: record.session.localId, browserId: record.id,
101
+ data: message.data, width: record.width, height: record.height };
102
+ viewers.forEach(function (id, ws) {
103
+ if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) ctx.sendTo(ws, record.frame);
104
+ });
105
+ visibility(record);
106
+ } else broadcast(record);
107
+ }
108
+ function create(session, control, newTab) {
109
+ requireAccess(session);
110
+ var tabs = sessionRecords(session);
111
+ var previous = !newTab && tabs.find(function (record) { return record.runtime && record.control === control; });
112
+ if (previous) return previous;
113
+ if (tabs.length >= 12) {
114
+ var expired = tabs.find(function (record) { return !record.runtime; });
115
+ if (expired) records.delete(expired.id);
116
+ else throw new Error("Close a browser tab before opening another");
117
+ }
118
+ var active = 0;
119
+ records.forEach(function (record) { if (record.runtime && record.ownerId === session.ownerId) active++; });
120
+ if (active >= 4) throw new Error("End another browser before opening more than four active tabs");
121
+ var record = { session: session, ownerId: session.ownerId, identity: identityKey(session), id: crypto.randomUUID(), phase: "starting", control: control, epoch: 0,
122
+ url: "", width: 1280, height: 800, touched: Date.now(), runtime: null, frame: null };
123
+ record.ready = new Promise(function (resolve, reject) { record.resolveReady = resolve; record.rejectReady = reject; });
124
+ record.ready.catch(function () {});
125
+ records.set(record.id, record);
126
+ try {
127
+ record.runtime = (ctx.createRuntime || createRuntime)({ identity: ctx.getIdentity(session), onEvent: function (message) { receive(record, message); } });
128
+ } catch (e) { end(record, e.message); }
129
+ broadcast(record, true);
130
+ return record;
131
+ }
132
+ async function changeControl(record, control) {
133
+ if (!record.runtime) throw new Error("Open a browser first");
134
+ record.control = control;
135
+ activityHistory.interruptActivity(record); record.pointer = null;
136
+ record.epoch++;
137
+ record.touched = Date.now();
138
+ record.handoff = true;
139
+ var epoch = record.epoch;
140
+ broadcast(record);
141
+ try {
142
+ await record.runtime.control(epoch);
143
+ if (record.runtime && record.epoch === epoch) { record.handoff = false; broadcast(record); }
144
+ } catch (e) { if (record.runtime && record.epoch === epoch) end(record, e.message); }
145
+
146
+ }
147
+ async function run(session, record, actor, action, event, intent) {
148
+ requireAccess(session);
149
+ if (!record || !record.runtime || !recordAllowed(record)) throw new Error("Open a browser first");
150
+ if (record.handoff) throw new Error("Browser control is changing; wait for the handoff");
151
+ if (action !== "inspect" && record.control !== actor) throw new Error(actor === "agent" ? "The user controls this browser. Ask them to choose Give control." : "The Driver controls this browser. Choose Take control first.");
152
+ var epoch = record.epoch;
153
+ var generation = Number(session._sdkQueryGeneration || 0);
154
+ await record.ready;
155
+ if (actor === "agent" && Number(session._sdkQueryGeneration || 0) !== generation) throw new Error("Browser request belongs to an older query");
156
+ requireAccess(session);
157
+ if (!recordAllowed(record) || !record.runtime || record.epoch !== epoch) throw new Error("Browser control changed; retry after the handoff");
158
+ record.touched = Date.now();
159
+ var result;
160
+ try { result = await record.runtime.request(action, event, epoch, actor === "agent" ? { intent: intent || "", generation: generation } : null); }
161
+ catch (error) {
162
+ if (actor === "agent" && record.epoch === epoch && record.activity && record.activity.phase === "running") {
163
+ activityHistory.recordActivity(record, Object.assign({}, record.activity, { phase: "failed" })); broadcast(record);
164
+ }
165
+ throw error;
166
+ }
167
+ requireAccess(session);
168
+ if (actor === "agent" && Number(session._sdkQueryGeneration || 0) !== generation) throw new Error("Browser result belongs to an older query");
169
+ if (!recordAllowed(record) || records.get(record.id) !== record || record.epoch !== epoch) throw new Error("Browser control changed during the action");
170
+ return result;
171
+ }
172
+ async function tool(session, args) {
173
+ try {
174
+ requireAccess(session);
175
+ if (args.intent !== undefined && (typeof args.intent !== "string" || args.intent.length > 160)) throw new Error("Browser intent must be at most 160 characters");
176
+ var record = args.action === "status" || args.action === "open" ? null : selectRecord(session, args.browserId);
177
+ var value;
178
+ if (args.action === "status") value = { browsers: sessionRecords(session).map(projection) };
179
+ else if (args.action === "open") {
180
+ var url = args.url ? policy.browserUrl(args.url) : null;
181
+ record = args.browserId ? selectRecord(session, args.browserId) : create(session, "agent", args.newTab === true);
182
+ if (url) await run(session, record, "agent", "action", { kind: "navigate", url: url }, args.intent);
183
+ value = projection(record);
184
+ } else if (args.action === "finish") {
185
+ if (!record || record.control !== "agent") throw new Error("The Driver does not control this browser");
186
+ await changeControl(record, "user"); value = projection(record);
187
+ } else if (args.action === "inspect") {
188
+ value = await run(session, record, "agent", "inspect", null, args.intent);
189
+ value.browserId = record.id;
190
+ var image = value.image; delete value.image;
191
+ return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "image", mimeType: "image/jpeg", data: image }] };
192
+ } else {
193
+ value = await run(session, record, "agent", "action", policy.validateAction(Object.assign({}, args, { kind: args.action })), args.intent);
194
+ }
195
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
196
+ } catch (e) { return { isError: true, content: [{ type: "text", text: e.message }] }; }
197
+ }
198
+ function handleMessage(ws, msg) {
199
+ if (!msg.type || msg.type.indexOf("shared_browser_") !== 0) return false;
200
+ var session = ctx.getSessionForWs(ws);
201
+ Promise.resolve().then(async function () {
202
+ requireAccess(session, ws);
203
+ if (String(msg.sessionId) !== String(session.localId)) throw new Error("Browser session changed");
204
+ var record;
205
+ if (msg.type === "shared_browser_state_request") { sendState(ws, session, sessionRecords(session)[0]); return; }
206
+ if (msg.type === "shared_browser_open") { record = create(session, "user", msg.newTab === true); sendState(ws, session, record, true, true); return; }
207
+ record = selectRecord(session, msg.browserId);
208
+ if (!record || !recordAllowed(record, ws) || msg.browserId !== record.id) throw new Error("Browser session expired");
209
+ if (msg.type === "shared_browser_view") {
210
+ if (msg.visible) { var previous = records.get(viewers.get(ws)); viewers.set(ws, record.id); if (previous && previous !== record) visibility(previous); if (record.frame) ctx.sendTo(ws, record.frame); }
211
+ else viewers.delete(ws);
212
+ visibility(record);
213
+ } else if (msg.type === "shared_browser_control") {
214
+ if (!["agent", "user"].includes(msg.control)) throw new Error("Invalid control mode");
215
+ await changeControl(record, msg.control);
216
+ } else if (msg.type === "shared_browser_close") {
217
+ end(record); records.delete(record.id);
218
+ sendState(ws, session, sessionRecords(session)[0]);
219
+ ctx.clients.forEach(function (peer) { if (peer !== ws && ctx.getSessionForWs(peer) === session) sendState(peer, session, sessionRecords(session)[0]); });
220
+ } else if (msg.type === "shared_browser_end") end(record);
221
+ else if (msg.type === "shared_browser_input") {
222
+ if (msg.epoch !== record.epoch) throw new Error("Browser control changed");
223
+ await run(session, record, "user", "action", policy.validateAction(msg.event));
224
+ }
225
+ }).catch(function (e) {
226
+ if (ws.readyState === 1) ctx.sendTo(ws, { type: "shared_browser_error", sessionId: msg.sessionId, error: e.message });
227
+ });
228
+ return true;
229
+ }
230
+ var sweep = setInterval(function () {
231
+ viewers.forEach(function (id, ws) { if (ws.readyState !== 1) viewers.delete(ws); });
232
+ records.forEach(function (record, key) {
233
+ if (!recordAllowed(record)) { end(record, "Browser session is no longer available"); records.delete(key); }
234
+ else if (Date.now() - record.touched > 30 * 60 * 1000) { end(record, "Browser expired after 30 minutes without activity"); records.delete(key); }
235
+ else visibility(record);
236
+ });
237
+ }, 15000);
238
+ sweep.unref();
239
+ function getToolDefs(session) {
240
+ var generation = Number(session && session._sdkQueryGeneration || 0);
241
+ return ctx.isMate ? [] : browserTools.getToolDefs(function (args) {
242
+ if (session && Number(session._sdkQueryGeneration || 0) !== generation) {
243
+ return Promise.resolve({ isError: true, content: [{ type: "text", text: "This browser tool belongs to an older query" }] });
244
+ }
245
+ return tool(session, args || {});
246
+ });
247
+ }
248
+ return {
249
+ handleMessage: handleMessage,
250
+ getToolDefs: getToolDefs,
251
+ getSystemPrompt: function () { return ctx.isMate ? "" : browserTools.PROMPT; },
252
+ createMcpServer: function (adapter, session) { return ctx.isMate || !adapter || typeof adapter.createToolServer !== "function" ? null : adapter.createToolServer({ name: "clay-shared-browser", version: "1.0.0", tools: getToolDefs(session) }); },
253
+ destroy: function () { destroyed = true; clearInterval(sweep); records.forEach(function (record) { end(record); }); records.clear(); viewers.clear(); },
254
+ };
255
+ }
256
+ module.exports = { attachSharedBrowser: attachSharedBrowser };
package/lib/project.js CHANGED
@@ -62,6 +62,7 @@ var { attachDriverContinuation } = require("./project-driver-continuation");
62
62
  var driverContinuationAccess = require("./driver-continuation-access");
63
63
  var { attachSessionNotes, composeSystemPrompts } = require("./project-session-notes");
64
64
  var { attachSessionDocument } = require("./project-session-document");
65
+ var { attachSharedBrowser } = require("./project-shared-browser");
65
66
  var { attachCapsuleCatalog } = require("./project-capsule-catalog");
66
67
  var { attachProjectWorkspaceQuery } = require("./project-workspace-query");
67
68
  var { attachProjectLogs } = require("./project-logs");
@@ -753,6 +754,15 @@ function createProjectContext(opts) {
753
754
  }
754
755
  },
755
756
  });
757
+ var _sharedBrowser = attachSharedBrowser({
758
+ slug: slug, isMate: isMate, sm: sm, clients: clients, sendTo: sendTo,
759
+ usersModule: usersModule, requestAccess: _requestAccess, osUsers: osUsers,
760
+ getSessionForWs: getSessionForWs,
761
+ getIdentity: function (session) {
762
+ var linuxUser = getLinuxUserForSession(session);
763
+ return linuxUser ? resolveOsUserInfo(linuxUser) : null;
764
+ },
765
+ });
756
766
  var _sessionDocument = attachSessionDocument({
757
767
  cwd: cwd,
758
768
  isMate: isMate,
@@ -832,6 +842,32 @@ function createProjectContext(opts) {
832
842
  return !usersModule.isMultiUser() || !!(ws._clayUser && ws._clayUser.id === session.ownerId);
833
843
  },
834
844
  });
845
+ var _sessionLinear = require("./project-session-linear").attachSessionLinear({
846
+ sm: sm, isMate: isMate, storage: opts.linearStorage, projectSlug: slug,
847
+ isMultiUser: usersModule.isMultiUser, sendTo: sendTo,
848
+ canAccess: function (ws) {
849
+ return !isMate && clients.has(ws) && ws.readyState === 1 && (!usersModule.isMultiUser() ||
850
+ !!(ws._clayUser && usersModule.findUserById(ws._clayUser.id) && opts.canAccessProjectSlug(ws._clayUser.id, slug)));
851
+ },
852
+ startWork: require('./project-linear-launch').attachLinearLaunch({
853
+ sm: sm, resolveDefaultAi: opts.resolveDefaultAi, getSdk: function () { return sdk; }, osUsers: osUsers,
854
+ getLinuxUserForSession: getLinuxUserForSession, ensureProjectAccessForSession: ensureProjectAccessForSession,
855
+ getProjectAccess: function () { return opts.getProjectAccess ? opts.getProjectAccess() : { visibility: 'public', ownerId: projectOwnerId || null }; },
856
+ onProcessingChanged: onProcessingChanged,
857
+ }),
858
+ canUse: function (session) {
859
+ if (session.hidden || session.delegated || session._pairDelegation || session._delegatedBy || require("./session-provenance").isWorker(session)) return false;
860
+ if (!usersModule.isMultiUser()) return true;
861
+ var owner = session.ownerId && usersModule.findUserById(session.ownerId);
862
+ return !!(owner && opts.canAccessProjectSlug && opts.canAccessProjectSlug(owner.id, slug));
863
+ },
864
+ canRead: function (caller, other) {
865
+ return !usersModule.isMultiUser() || usersModule.canAccessSession(caller.ownerId, other, opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public" });
866
+ },
867
+ canRefresh: function (ws, session) {
868
+ return !usersModule.isMultiUser() || !!(ws._clayUser && ws._clayUser.id === session.ownerId);
869
+ },
870
+ });
835
871
  var _mateKnowledge = attachProjectMateKnowledge({
836
872
  service: opts.mateKnowledgeService || null,
837
873
  sm: sm,
@@ -1014,6 +1050,8 @@ function createProjectContext(opts) {
1014
1050
  // get the read-only cross-project set only when the Mate registry confirms
1015
1051
  // authoritative builtin Clay.
1016
1052
  try {
1053
+ var linearMcpConfig = _sessionLinear.createMcpServer(adapter, null);
1054
+ if (linearMcpConfig) servers['clay-linear'] = linearMcpConfig;
1017
1055
  var githubMcpConfig = _sessionGithub.createMcpServer(adapter, null);
1018
1056
  if (githubMcpConfig) servers["clay-github"] = githubMcpConfig;
1019
1057
  var issuesMcpConfig = _issues.createMcpServer(adapter, null);
@@ -1045,6 +1083,9 @@ function createProjectContext(opts) {
1045
1083
  }
1046
1084
  }
1047
1085
 
1086
+ var sharedBrowserConfig = _sharedBrowser.createMcpServer(adapter);
1087
+ if (sharedBrowserConfig) servers["clay-shared-browser"] = sharedBrowserConfig;
1088
+
1048
1089
  // Explicit agent signal for user-requested Markdown presentation.
1049
1090
  if (!isMate) {
1050
1091
  try {
@@ -1199,6 +1240,11 @@ function createProjectContext(opts) {
1199
1240
  }
1200
1241
  continue;
1201
1242
  }
1243
+ if (name === "clay-linear" && forSession) {
1244
+ var boundLinear = _sessionLinear.createMcpServer(adapter, forSession);
1245
+ if (boundLinear) { filtered[name] = boundLinear; hasAny = true; }
1246
+ continue;
1247
+ }
1202
1248
  if (name === "clay-github" && forSession) {
1203
1249
  var boundGithub = _sessionGithub.createMcpServer(adapter, forSession);
1204
1250
  if (boundGithub) { filtered[name] = boundGithub; hasAny = true; }
@@ -1263,6 +1309,11 @@ function createProjectContext(opts) {
1263
1309
  }
1264
1310
  continue;
1265
1311
  }
1312
+ if (name === "clay-shared-browser" && forSession) {
1313
+ var boundBrowser = _sharedBrowser.createMcpServer(adapter, forSession);
1314
+ if (boundBrowser) { filtered[name] = boundBrowser; hasAny = true; }
1315
+ continue;
1316
+ }
1266
1317
  if (name === "clay-documents" && forSession) {
1267
1318
  try {
1268
1319
  var boundDocuments = _sessionDocument.createMcpServer(adapter, forSession);
@@ -1374,10 +1425,12 @@ function createProjectContext(opts) {
1374
1425
  _sessionHandoff.getSystemPrompt(session),
1375
1426
  _driverContinuation.getSystemPrompt(session),
1376
1427
  _sessionDocument.getSystemPrompt(session),
1428
+ _sharedBrowser.getSystemPrompt(session),
1377
1429
  _capsuleCatalog.getSystemPrompt(session),
1378
1430
  _projectLogs.getSystemPrompt(session),
1379
1431
  _issues.getSystemPrompt(session),
1380
1432
  _sessionGithub.getSystemPrompt(session),
1433
+ _sessionLinear.getSystemPrompt(session),
1381
1434
  _scheduledTasks ? _scheduledTasks.getSystemPrompt(session) : "",
1382
1435
  _mateKnowledge.getSystemPrompt(session),
1383
1436
  _autonomousRun ? _autonomousRun.getSystemPrompt(session) : "",
@@ -1393,6 +1446,7 @@ function createProjectContext(opts) {
1393
1446
  .concat(_sessionHandoff.getToolDefs(session))
1394
1447
  .concat(_driverContinuation.getToolDefs(session, Number(session._sdkQueryGeneration || 0)))
1395
1448
  .concat(_sessionDocument.getToolDefs(session))
1449
+ .concat(_sharedBrowser.getToolDefs(session))
1396
1450
  .concat(_debateProposal.getToolDefs(session))
1397
1451
  .concat(_mateCreationProposal.getToolDefs(session))
1398
1452
  .concat(_workspaceQuery.getDynamicToolDefs(session))
@@ -1400,6 +1454,7 @@ function createProjectContext(opts) {
1400
1454
  .concat(_projectLogs.getDynamicToolDefs(session))
1401
1455
  .concat(_issues.getDynamicToolDefs(session))
1402
1456
  .concat(_sessionGithub.getDynamicToolDefs(session))
1457
+ .concat(_sessionLinear.getDynamicToolDefs(session))
1403
1458
  .concat(_mateKnowledge.getDynamicToolDefs(session))
1404
1459
  .concat(_loopInterview ? _loopInterview.getToolDefs(session) : [])
1405
1460
  .concat(_scheduledTasks ? _scheduledTasks.getToolDefs(session) : [])
@@ -1792,6 +1847,7 @@ function createProjectContext(opts) {
1792
1847
  if (_projectLogs.handleLogsMessage(ws, msg)) return;
1793
1848
  if (_issues.handleMessage(ws, msg)) return;
1794
1849
  if (_sessionGithub.handleMessage(ws, msg)) return;
1850
+ if (_sessionLinear.handleMessage(ws, msg)) return;
1795
1851
 
1796
1852
  // --- Notifications (delegated to project-notifications.js) ---
1797
1853
  if (_notifications.handleNotificationMessage(ws, msg)) return;
@@ -1821,6 +1877,7 @@ function createProjectContext(opts) {
1821
1877
  if (_shellCommand.handleShellCommand(ws, msg)) return;
1822
1878
 
1823
1879
  // --- Vendor login flow (delegated to project-vendor-login.js) ---
1880
+ if (_sharedBrowser.handleMessage(ws, msg)) return;
1824
1881
  if (_vendorLogin.handleVendorLoginMessage(ws, msg)) return;
1825
1882
 
1826
1883
  // --- Notes, terminals, context, user message (delegated to project-user-message.js) ---
@@ -2344,6 +2401,11 @@ function createProjectContext(opts) {
2344
2401
  inputSchema: normalizeToolSchema(continuationTools[cti].inputSchema),
2345
2402
  });
2346
2403
  }
2404
+ var sharedTools = _sharedBrowser.getToolDefs(boundSession);
2405
+ sharedTools.forEach(function (definition) {
2406
+ tools.push({ server: "clay-shared-browser", name: definition.name, description: definition.description,
2407
+ inputSchema: normalizeToolSchema(definition.inputSchema) });
2408
+ });
2347
2409
  var documentTools = _sessionDocument.getToolDefs(boundSession);
2348
2410
  for (var dti = 0; dti < documentTools.length; dti++) {
2349
2411
  tools.push({
@@ -2363,6 +2425,7 @@ function createProjectContext(opts) {
2363
2425
  for (var cht = 0; cht < historyTools.length; cht++) tools.push(historyTools[cht]);
2364
2426
  tools = tools.concat(_issues.getBridgeTools(boundSession, normalizeToolSchema));
2365
2427
  tools = tools.concat(_sessionGithub.getBridgeTools(boundSession, normalizeToolSchema));
2428
+ tools = tools.concat(_sessionLinear.getBridgeTools(boundSession, normalizeToolSchema));
2366
2429
  var logsTools = _projectLogs.getBridgeTools(boundSession, normalizeToolSchema);
2367
2430
  for (var plti = 0; plti < logsTools.length; plti++) tools.push(logsTools[plti]);
2368
2431
  var knowledgeTools = _mateKnowledge.getBridgeTools(boundSession, normalizeToolSchema);
@@ -2387,10 +2450,11 @@ function createProjectContext(opts) {
2387
2450
  var inAppNames = Object.keys(localMcp);
2388
2451
  for (var i = 0; i < inAppNames.length; i++) {
2389
2452
  if (inAppNames[i] === "clay-sessions" || inAppNames[i] === "clay-notes" || inAppNames[i] === "clay-handoff" || inAppNames[i] === "clay-documents") continue;
2453
+ if (inAppNames[i] === "clay-shared-browser") continue;
2390
2454
  // Debate tools were added above only when this request had an
2391
2455
  // exact session. Never duplicate them or advertise the static,
2392
2456
  // intentionally unbound proposal server.
2393
- if (inAppNames[i] === "clay-debate" || inAppNames[i] === "clay-mate-creation" || inAppNames[i] === "clay-workspace" || inAppNames[i] === "clay-history" || inAppNames[i] === "clay-github" || inAppNames[i] === "clay-issues" || inAppNames[i] === "clay-logs" || inAppNames[i] === "clay-knowledge") continue;
2457
+ if (inAppNames[i] === "clay-debate" || inAppNames[i] === "clay-mate-creation" || inAppNames[i] === "clay-workspace" || inAppNames[i] === "clay-history" || inAppNames[i] === "clay-github" || inAppNames[i] === "clay-linear" || inAppNames[i] === "clay-issues" || inAppNames[i] === "clay-logs" || inAppNames[i] === "clay-knowledge") continue;
2394
2458
  extractServerTools(inAppNames[i], localMcp[inAppNames[i]]);
2395
2459
  }
2396
2460
  }
@@ -2446,6 +2510,12 @@ function createProjectContext(opts) {
2446
2510
  }
2447
2511
  }
2448
2512
  }
2513
+ if (boundSession && serverName === "clay-shared-browser") {
2514
+ var sharedTools = _sharedBrowser.getToolDefs(boundSession);
2515
+ for (var bti = 0; bti < sharedTools.length; bti++) {
2516
+ if (sharedTools[bti].name === toolName) return Promise.resolve(sharedTools[bti].handler(args || {}));
2517
+ }
2518
+ }
2449
2519
  if (boundSession && serverName === "clay-documents") {
2450
2520
  var documentTools = _sessionDocument.getToolDefs(boundSession);
2451
2521
  for (var dti = 0; dti < documentTools.length; dti++) {
@@ -2476,6 +2546,7 @@ function createProjectContext(opts) {
2476
2546
  if (boundSession && serverName === "clay-history") {
2477
2547
  return _workspaceQuery.callHistoryBridgeTool(boundSession, toolName, args);
2478
2548
  }
2549
+ if (boundSession && serverName === "clay-linear") return _sessionLinear.callBridgeTool(boundSession, toolName, args);
2479
2550
  if (boundSession && serverName === "clay-github") return _sessionGithub.callBridgeTool(boundSession, toolName, args);
2480
2551
  if (boundSession && serverName === "clay-issues") return _issues.callBridgeTool(boundSession, toolName, args);
2481
2552
  if (boundSession && serverName === "clay-logs") {
@@ -2484,7 +2555,7 @@ function createProjectContext(opts) {
2484
2555
  if (boundSession && serverName === "clay-knowledge") {
2485
2556
  return _mateKnowledge.callBridgeTool(boundSession, toolName, args);
2486
2557
  }
2487
- if (serverName === "clay-sessions" || serverName === "clay-notes" || serverName === "clay-handoff" || serverName === "clay-documents" || serverName === "clay-debate" || serverName === "clay-mate-creation" || serverName === "clay-workspace" || serverName === "clay-history" || serverName === "clay-github" || serverName === "clay-issues" || serverName === "clay-logs" || serverName === "clay-knowledge") {
2558
+ if (serverName === "clay-sessions" || serverName === "clay-notes" || serverName === "clay-handoff" || serverName === "clay-documents" || serverName === "clay-shared-browser" || serverName === "clay-debate" || serverName === "clay-mate-creation" || serverName === "clay-workspace" || serverName === "clay-history" || serverName === "clay-github" || serverName === "clay-linear" || serverName === "clay-issues" || serverName === "clay-logs" || serverName === "clay-knowledge") {
2488
2559
  return Promise.reject(new Error("Session-bound tool requires a valid Clay session: " + serverName + "/" + toolName));
2489
2560
  }
2490
2561
  if (sessionOnly) return Promise.reject(new Error("Session tool not found: " + serverName + "/" + toolName));
@@ -2602,6 +2673,7 @@ function createProjectContext(opts) {
2602
2673
 
2603
2674
  // --- Destroy ---
2604
2675
  function destroy() {
2676
+ _sharedBrowser.destroy();
2605
2677
  _vendorLogin.destroy();
2606
2678
  if (_scheduledMessages) _scheduledMessages.shutdown();
2607
2679
  if (_autonomousRun) _autonomousRun.shutdown();
package/lib/public/app.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { initSharedBrowser } from './modules/shared-browser.js';
1
2
  import { initGenerationStop } from './modules/generation-stop.js';
2
3
  import { initIssueLinks } from './modules/clay-issue-links.js';
3
4
  import { initIssues, closeIssues } from './modules/issues.js';
@@ -269,6 +270,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
269
270
  createStore({
270
271
  networkSettings: { loaded: false, busy: false, confirming: false, draft: "", saved: "", status: "", errorKind: "", requestId: 0 },
271
272
  vendorLoginBrowser: null,
273
+ sharedBrowser: null,
274
+ sharedBrowsers: [],
275
+ mcpSkillsWorkbench: {},
276
+ sharedBrowserOpen: false,
277
+ sharedBrowserUi: {},
272
278
  // session / routing
273
279
  basePath: basePath,
274
280
  wsPath: wsPath,
@@ -1175,6 +1181,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1175
1181
  fileViewerEl: $("file-viewer"),
1176
1182
  });
1177
1183
  initGitPanel();
1184
+ initSharedBrowser();
1178
1185
  initWorkerPaneLock();
1179
1186
 
1180
1187
  // --- Terminal ---
@@ -1054,12 +1054,6 @@
1054
1054
  flex-direction: column;
1055
1055
  overflow: hidden;
1056
1056
  flex-shrink: 0;
1057
- box-shadow:
1058
- 4px 8px 18px rgba(var(--shadow-rgb), 0.14),
1059
- 2px 2px 4px rgba(var(--shadow-rgb), 0.08),
1060
- inset 0 1px 0 rgba(255, 255, 255, 0.04);
1061
- animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
1062
- transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
1063
1057
  }
1064
1058
  #file-viewer.hidden { display: none; }
1065
1059
  #file-viewer.file-viewer-wide {
@@ -1078,15 +1072,10 @@
1078
1072
  border: 0;
1079
1073
  border-radius: 0;
1080
1074
  background: var(--bg);
1081
- box-shadow: none;
1082
- animation: none;
1083
1075
  }
1084
1076
  }
1085
1077
 
1086
- @keyframes workbench-panel-in {
1087
- from { opacity: 0; transform: translateX(10px) scale(0.995); }
1088
- to { opacity: 1; transform: translateX(0) scale(1); }
1089
- }
1078
+
1090
1079
 
1091
1080
  /* Narrow screens: full overlay */
1092
1081
  @media (max-width: 1023px) {
@@ -2026,11 +2015,6 @@
2026
2015
  flex-direction: column;
2027
2016
  overflow: hidden;
2028
2017
  flex-shrink: 0;
2029
- box-shadow:
2030
- 4px 8px 18px rgba(var(--shadow-rgb), 0.14),
2031
- 2px 2px 4px rgba(var(--shadow-rgb), 0.08),
2032
- inset 0 1px 0 rgba(255, 255, 255, 0.04);
2033
- animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
2034
2018
  }
2035
2019
  #terminal-container.hidden { display: none; }
2036
2020
  #terminal-container.panel-fullscreen {
@@ -2041,17 +2025,10 @@
2041
2025
  margin: 0;
2042
2026
  border: 0;
2043
2027
  border-radius: 0;
2044
- box-shadow: none;
2045
- animation: none;
2046
2028
  }
2047
2029
  }
2048
2030
 
2049
- @media (prefers-reduced-motion: reduce) {
2050
- #file-viewer,
2051
- #terminal-container {
2052
- animation: none;
2053
- }
2054
- }
2031
+
2055
2032
 
2056
2033
  /* Narrow screens: full overlay */
2057
2034
  @media (max-width: 1023px) {
@@ -1,20 +1,7 @@
1
1
  #issues-panel { position: relative; width: 50%; max-width: 720px; min-width: 360px; height: calc(100% - 16px); margin: 8px 8px 8px 10px; min-height: 0; align-self: center; display: flex; flex-direction: column; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; flex-shrink: 0; }
2
- @media (min-width: 1024px) {
3
- #issues-panel {
4
- box-shadow:
5
- 4px 8px 18px rgba(var(--shadow-rgb), 0.14),
6
- 2px 2px 4px rgba(var(--shadow-rgb), 0.08),
7
- inset 0 1px 0 rgba(255, 255, 255, 0.04);
8
- animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
9
- transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
10
- }
11
- }
12
- @media (prefers-reduced-motion: reduce) {
13
- #issues-panel { animation: none; }
14
- }
15
2
  #issues-panel.hidden { display: none; }
16
3
  #issues-panel.issues-wide { width: 70%; max-width: 1200px; }
17
- #issues-panel.panel-fullscreen { width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; box-shadow: none; animation: none; }
4
+ #issues-panel.panel-fullscreen { width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; }
18
5
  #issues-panel header { height: 48px; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; }
19
6
  #issues-panel .issues-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; }
20
7
  #issues-panel .issues-title .lucide { width: 15px; color: var(--accent); }
@@ -0,0 +1,35 @@
1
+ #linear-panel { width: 45%; max-width: 720px; min-width: 340px; min-height: 0; height: calc(100% - 16px); margin: 8px; align-self: center; display: flex; flex-direction: column; flex-shrink: 0; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); color: var(--text); overflow: hidden; }
2
+ #linear-panel.hidden { display: none; }
3
+ .linear-topbar, .linear-search, .linear-actions { display: flex; align-items: center; gap: 8px; }
4
+ .linear-topbar { padding: 10px 12px; border-bottom: 1px solid var(--border); }
5
+ .linear-topbar strong { flex: 1; }
6
+ #linear-panel button { font: inherit; color: var(--text-secondary); background: var(--bg-alt); border: 1px solid var(--border); border-radius: 6px; padding: 6px 9px; cursor: pointer; }
7
+ #linear-panel button:hover { background: var(--input-bg); color: var(--text); }
8
+ #linear-panel button:disabled { opacity: .5; cursor: wait; }
9
+ #linear-panel button:focus-visible, #linear-panel input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
10
+ .linear-topbar svg { width: 16px; height: 16px; }
11
+ .linear-search { padding: 12px; }
12
+ .linear-search input { width: 100%; min-width: 0; padding: 8px; background: var(--input-bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font: inherit; }
13
+ .linear-notice { margin: 0; padding: 0 12px; color: var(--text-muted); font-size: 12px; }
14
+ .linear-notice:not(:empty) { padding-bottom: 10px; }
15
+ .linear-content { overflow: auto; min-height: 0; flex: 1; }
16
+ .linear-empty { padding: 20px; color: var(--text-muted); }
17
+ .linear-actions { flex-wrap: wrap; font-size: 12px; }
18
+ #linear-panel a { color: var(--link); }
19
+ #linear-panel .issue-row { width: 100%; text-align: left; }
20
+ #linear-panel .issue-row small, #linear-panel .issue-row strong { display: block; }
21
+ #linear-panel .issue-discussion article { padding: 14px 0; border-top: 1px solid var(--border-subtle); }
22
+ #linear-panel .issue-doc-body { overflow-wrap: anywhere; }
23
+ #linear-panel .issue-doc-body pre { overflow-x: auto; }
24
+ .session-linear-link { max-width: 360px; min-width: 0; flex-shrink: 1; }
25
+ .session-linear-link > span { white-space: nowrap; }
26
+ .session-linear-link .linear-work-id, .session-linear-link .linear-work-state { flex-shrink: 0; }
27
+ .session-linear-link .linear-work-title { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
28
+ .linear-work-state { color: var(--text-secondary); }
29
+ #header-github-links:has(.session-linear-link) { max-width: min(440px, 40vw); }
30
+ #header-github-links:has(.session-linear-link) > .session-github-links { display: flex; }
31
+ @media (max-width: 767px) {
32
+ #header-github-links:has(.session-linear-link) { max-width: 170px; }
33
+ #header-github-links .linear-work-title, #header-github-links .linear-work-state { display: none; }
34
+ }
35
+ @media (max-width: 767px) { #linear-panel { position: fixed; inset: 8px; width: auto; max-width: none; min-width: 0; height: auto; z-index: 300; margin: 0; } }