clay-server 4.7.1-beta.5 → 4.8.0-beta.2

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 (55) hide show
  1. package/bin/login-browser-open.js +7 -0
  2. package/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
  3. package/lib/clay-skill-install.js +33 -0
  4. package/lib/clay-skill-storage.js +49 -0
  5. package/lib/project-http.js +14 -5
  6. package/lib/project-shared-browser.js +256 -0
  7. package/lib/project-vendor-login.js +52 -18
  8. package/lib/project.js +38 -1
  9. package/lib/public/app.js +8 -0
  10. package/lib/public/css/filebrowser.css +2 -25
  11. package/lib/public/css/issues.css +1 -14
  12. package/lib/public/css/mcp-skills-workbench.css +51 -0
  13. package/lib/public/css/project-logs.css +0 -11
  14. package/lib/public/css/right-workbench.css +28 -0
  15. package/lib/public/css/scheduled-tasks.css +1 -1
  16. package/lib/public/css/shared-browser-skins.css +28 -0
  17. package/lib/public/css/shared-browser.css +91 -0
  18. package/lib/public/css/sticky-notes.css +0 -11
  19. package/lib/public/css/vendor-login-browser.css +63 -0
  20. package/lib/public/index.html +30 -22
  21. package/lib/public/modules/app-messages.js +7 -0
  22. package/lib/public/modules/app-notifications.js +1 -1
  23. package/lib/public/modules/extension-settings.js +110 -0
  24. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  25. package/lib/public/modules/mcp-ui.js +9 -12
  26. package/lib/public/modules/notifications.js +2 -107
  27. package/lib/public/modules/shared-browser-activity.js +66 -0
  28. package/lib/public/modules/shared-browser-input.js +73 -0
  29. package/lib/public/modules/shared-browser-tabs.js +33 -0
  30. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  31. package/lib/public/modules/shared-browser.js +214 -0
  32. package/lib/public/modules/sidebar-mobile.js +3 -3
  33. package/lib/public/modules/skills-actions.js +247 -0
  34. package/lib/public/modules/skills-detail.js +148 -0
  35. package/lib/public/modules/skills-state.js +10 -0
  36. package/lib/public/modules/skills.js +64 -459
  37. package/lib/public/modules/tool-palette-order.js +11 -13
  38. package/lib/public/modules/vendor-login-browser.js +243 -0
  39. package/lib/public/modules/vendor-login.js +23 -4
  40. package/lib/public/style.css +7 -0
  41. package/lib/sdk-bridge.js +14 -1
  42. package/lib/sessions.js +2 -0
  43. package/lib/shared-browser-activity-history.js +17 -0
  44. package/lib/shared-browser-policy.js +26 -0
  45. package/lib/shared-browser-runtime.js +82 -0
  46. package/lib/shared-browser-tools.js +21 -0
  47. package/lib/shared-browser-worker.js +160 -0
  48. package/lib/terminal.js +4 -0
  49. package/lib/vendor-login-browser-flow.js +46 -0
  50. package/lib/vendor-login-browser-passkeys.js +27 -0
  51. package/lib/vendor-login-browser-worker.js +149 -0
  52. package/lib/vendor-login-browser.js +162 -0
  53. package/lib/ws-schema.js +14 -0
  54. package/lib/yoke/skill-discovery.js +22 -2
  55. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  // Vendor login flow coordinator.
2
2
  //
3
3
  // Owns the "the vendor CLI is not logged in" recovery path end to end so the
4
- // client never has to guess. One login terminal per vendor per project is the
4
+ // client never has to guess. One login terminal per vendor per requester is the
5
5
  // single source of truth: repeated auth_required events (other sessions, split
6
6
  // panes, a burst of 401 retries) re-use that record instead of spawning more
7
7
  // terminals.
@@ -11,7 +11,7 @@
11
11
  // command, reply with vendor_login_ready
12
12
  // PTY output/exit -> detect a successful login, restart the vendor's
13
13
  // YOKE adapter so it re-reads the credential file,
14
- // broadcast auth_refreshed, close the terminal
14
+ // send auth_refreshed, close the terminal and browser
15
15
  // vendor_login_cancel -> user dismissed the flow; kill the terminal
16
16
  //
17
17
  // The adapter restart is the actual bug fix: a Codex app-server is a long-lived
@@ -84,10 +84,20 @@ function attachVendorLogin(ctx) {
84
84
  var getLinuxUserForSession = ctx.getLinuxUserForSession;
85
85
  var refreshVendorAuthEverywhere = ctx.refreshVendorAuthEverywhere;
86
86
 
87
- // vendor -> flow record. At most one live login terminal per vendor.
87
+ // Requester + vendor -> flow. Credentials and sign-in views stay user-bound.
88
88
  var _flows = Object.create(null);
89
+ function flowKey(ws, vendor) {
90
+ return JSON.stringify([ws && ws._clayUser ? String(ws._clayUser.id) : null, vendor]);
91
+ }
92
+ var browserLogin = require("./vendor-login-browser-flow").attachBrowserLogin({
93
+ osUsers: osUsers, sendTo: sendTo, hasTerminalPermission: hasTerminalPermission,
94
+ createLoginBrowser: ctx.createLoginBrowser,
95
+ flowKey: flowKey,
96
+ cancel: function (vendor) { closeFlowTerminal(discardFlow(vendor)); broadcastState(); },
97
+ });
89
98
 
90
99
  function loginCommandFor(vendor) {
100
+ if (vendor === "claude") return "claude auth login";
91
101
  var info = vendorRegistry.getVendorInfo(vendor);
92
102
  return (info && info.loginCommand) || "claude login";
93
103
  }
@@ -97,30 +107,34 @@ function attachVendorLogin(ctx) {
97
107
  return (info && info.displayName) || vendor || "Vendor";
98
108
  }
99
109
 
100
- function listFlows() {
110
+ function listFlows(ws) {
101
111
  var result = [];
102
112
  var vendors = Object.keys(_flows);
103
113
  for (var i = 0; i < vendors.length; i++) {
104
114
  var flow = _flows[vendors[i]];
115
+ if (ws && flowKey(ws, flow.vendor) !== vendors[i]) continue;
105
116
  result.push({
106
117
  vendor: flow.vendor,
107
118
  terminalId: flow.terminalId,
108
119
  startedAt: flow.startedAt,
109
120
  completed: !!flow.completed,
121
+ browserId: flow.browser ? flow.browser.id : null,
110
122
  });
111
123
  }
112
124
  return result;
113
125
  }
114
126
 
115
127
  function broadcastState() {
128
+ if (ctx.clients) { ctx.clients.forEach(sendStateTo); return; }
116
129
  send({ type: "vendor_login_state", slug: slug, flows: listFlows() });
117
130
  }
118
131
 
119
132
  function sendStateTo(ws) {
120
- sendTo(ws, { type: "vendor_login_state", slug: slug, flows: listFlows() });
133
+ sendTo(ws, { type: "vendor_login_state", slug: slug, flows: hasTerminalPermission(ws) ? listFlows(ws) : [] });
121
134
  }
122
135
 
123
136
  function hasTerminalPermission(ws) {
137
+ if (ctx.requestAccess) return ctx.requestAccess.canAccessProject(ws, slug) && ctx.requestAccess.hasPermission(ws, "terminal");
124
138
  if (!ws || !ws._clayUser) return true;
125
139
  if (!usersModule || typeof usersModule.getEffectivePermissions !== "function") return true;
126
140
  var perms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
@@ -164,6 +178,8 @@ function attachVendorLogin(ctx) {
164
178
  var flow = _flows[vendor];
165
179
  if (!flow) return null;
166
180
  delete _flows[vendor];
181
+ if (flow.browser) flow.browser.close();
182
+ if (flow.expiryTimer) clearTimeout(flow.expiryTimer);
167
183
  if (flow.closeTimer) {
168
184
  clearTimeout(flow.closeTimer);
169
185
  flow.closeTimer = null;
@@ -223,14 +239,17 @@ function attachVendorLogin(ctx) {
223
239
  var refresh = Promise.resolve(false);
224
240
  if (succeeded) {
225
241
  refresh = typeof refreshVendorAuthEverywhere === "function"
226
- ? refreshVendorAuthEverywhere(vendor, flow.linuxUser || null)
227
- : refreshVendorAuth(vendor, flow.linuxUser || null);
242
+ ? refreshVendorAuthEverywhere(flow.vendor, flow.linuxUser || null)
243
+ : refreshVendorAuth(flow.vendor, flow.linuxUser || null);
228
244
  }
229
245
  refresh.then(function (restarted) {
246
+ if (_flows[vendor] !== flow) return;
230
247
  discardFlow(vendor);
231
248
  closeFlowTerminal(flow);
232
249
  if (succeeded) {
233
- send({ type: "auth_refreshed", slug: slug, vendor: vendor, adapterRestarted: !!restarted });
250
+ var message = { type: "auth_refreshed", slug: slug, vendor: flow.vendor, adapterRestarted: !!restarted };
251
+ if (ctx.clients) ctx.clients.forEach(function (ws) { if (flowKey(ws, flow.vendor) === vendor && hasTerminalPermission(ws)) sendTo(ws, message); });
252
+ else send(message);
234
253
  }
235
254
  broadcastState();
236
255
  });
@@ -240,10 +259,11 @@ function attachVendorLogin(ctx) {
240
259
  var flow = _flows[vendor];
241
260
  if (!flow || flow.finishing || flow.succeeded) return;
242
261
  flow.outputTail = (flow.outputTail + stripAnsi(chunk)).slice(-OUTPUT_TAIL_MAX);
262
+ if (flow.browser) flow.browser.output(stripAnsi(chunk));
243
263
  if (!looksLikeLoginSuccess(flow.outputTail)) return;
244
264
 
245
265
  flow.succeeded = true;
246
- console.log("[vendor-login] Detected successful " + vendor + " login in " + slug);
266
+ console.log("[vendor-login] Detected successful " + flow.vendor + " login in " + slug);
247
267
  // Let the success line land on screen before the terminal disappears.
248
268
  flow.closeTimer = setTimeout(function () {
249
269
  finishFlow(vendor, true);
@@ -261,10 +281,15 @@ function attachVendorLogin(ctx) {
261
281
  }
262
282
 
263
283
  function startFlow(ws, vendor, msg) {
284
+ var key = flowKey(ws, vendor);
264
285
  var identity = resolveLoginIdentity(ws, msg);
265
286
  var command = loginCommandFor(vendor);
287
+ var browserMode = msg.mode === "browser" && (vendor === "claude" || vendor === "codex");
288
+ if (browserMode) command = vendor === "codex" ? "codex login" : "claude auth login";
266
289
  var flow = {
267
290
  vendor: vendor,
291
+ key: key,
292
+ ownerId: ws && ws._clayUser ? String(ws._clayUser.id) : null,
268
293
  terminalId: null,
269
294
  startedAt: Date.now(),
270
295
  linuxUser: identity.linuxUser,
@@ -277,16 +302,18 @@ function attachVendorLogin(ctx) {
277
302
  };
278
303
  // Registered before create() so the PTY's first output chunk (hooks fire
279
304
  // synchronously from spawn) already finds the record.
280
- _flows[vendor] = flow;
305
+ _flows[key] = flow;
281
306
 
282
307
  var terminal = null;
283
308
  try {
309
+ if (browserMode) browserLogin.prepare(ws, flow, identity);
284
310
  terminal = tm.create(100, 30, identity.osUserInfo, ws, {
285
311
  initialInput: command + "\n",
286
312
  title: vendorDisplayName(vendor) + " login",
287
313
  kind: "vendor-login",
288
- onData: function (data) { handleFlowOutput(vendor, data); },
289
- onExit: function () { handleFlowExit(vendor); },
314
+ loginBrowserEnv: flow.browser ? flow.browser.env : null,
315
+ onData: function (data) { if (_flows[key] === flow) handleFlowOutput(key, data); },
316
+ onExit: function () { if (_flows[key] === flow) handleFlowExit(key); },
290
317
  });
291
318
  } catch (e) {
292
319
  console.error("[vendor-login] Failed to spawn " + vendor + " login terminal in " + slug + ":",
@@ -294,17 +321,18 @@ function attachVendorLogin(ctx) {
294
321
  }
295
322
 
296
323
  if (!terminal) {
297
- delete _flows[vendor];
324
+ discardFlow(key);
298
325
  return null;
299
326
  }
300
327
 
301
328
  flow.terminalId = terminal.id;
302
- tm.attach(terminal.id, ws);
329
+ if (!flow.browser) tm.attach(terminal.id, ws);
303
330
  send({ type: "term_list", terminals: tm.list() });
304
331
  return flow;
305
332
  }
306
333
 
307
334
  function handleVendorLoginMessage(ws, msg) {
335
+ if (browserLogin.handle(ws, msg, _flows)) return true;
308
336
  if (msg.type === "vendor_login_state_request") {
309
337
  sendStateTo(ws);
310
338
  return true;
@@ -312,7 +340,7 @@ function attachVendorLogin(ctx) {
312
340
 
313
341
  if (msg.type === "vendor_login_cancel") {
314
342
  var cancelVendor = String(msg.vendor || "");
315
- var cancelled = discardFlow(cancelVendor);
343
+ var cancelled = discardFlow(flowKey(ws, cancelVendor));
316
344
  if (cancelled) closeFlowTerminal(cancelled);
317
345
  broadcastState();
318
346
  return true;
@@ -329,10 +357,11 @@ function attachVendorLogin(ctx) {
329
357
  return true;
330
358
  }
331
359
 
332
- var existing = _flows[vendor];
360
+ var key = flowKey(ws, vendor);
361
+ var existing = _flows[key];
333
362
  if (existing && !isFlowTerminalAlive(existing)) {
334
363
  // Terminal died without its exit hook clearing the record.
335
- discardFlow(vendor);
364
+ discardFlow(key);
336
365
  existing = null;
337
366
  }
338
367
 
@@ -341,13 +370,14 @@ function attachVendorLogin(ctx) {
341
370
  // session or split pane) must not pop a second prompt; it only learns
342
371
  // that a flow is already running. A deliberate request re-attaches.
343
372
  if (!msg.auto) {
344
- tm.attach(existing.terminalId, ws);
373
+ if (!existing.browser) tm.attach(existing.terminalId, ws);
345
374
  sendTo(ws, {
346
375
  type: "vendor_login_ready",
347
376
  slug: slug,
348
377
  vendor: vendor,
349
378
  terminalId: existing.terminalId,
350
379
  reused: true,
380
+ browserId: existing.browser ? existing.browser.id : null,
351
381
  });
352
382
  }
353
383
  sendStateTo(ws);
@@ -371,6 +401,7 @@ function attachVendorLogin(ctx) {
371
401
  vendor: vendor,
372
402
  terminalId: started.terminalId,
373
403
  reused: false,
404
+ browserId: started.browser ? started.browser.id : null,
374
405
  });
375
406
  broadcastState();
376
407
  return true;
@@ -408,6 +439,9 @@ function attachVendorLogin(ctx) {
408
439
  forgetTerminal: forgetTerminal,
409
440
  isLoginTerminal: isLoginTerminal,
410
441
  refreshVendorAuth: refreshVendorAuth,
442
+ destroy: function () {
443
+ Object.keys(_flows).forEach(function (vendor) { closeFlowTerminal(discardFlow(vendor)); });
444
+ },
411
445
  };
412
446
  }
413
447
 
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,
@@ -1045,6 +1055,9 @@ function createProjectContext(opts) {
1045
1055
  }
1046
1056
  }
1047
1057
 
1058
+ var sharedBrowserConfig = _sharedBrowser.createMcpServer(adapter);
1059
+ if (sharedBrowserConfig) servers["clay-shared-browser"] = sharedBrowserConfig;
1060
+
1048
1061
  // Explicit agent signal for user-requested Markdown presentation.
1049
1062
  if (!isMate) {
1050
1063
  try {
@@ -1263,6 +1276,11 @@ function createProjectContext(opts) {
1263
1276
  }
1264
1277
  continue;
1265
1278
  }
1279
+ if (name === "clay-shared-browser" && forSession) {
1280
+ var boundBrowser = _sharedBrowser.createMcpServer(adapter, forSession);
1281
+ if (boundBrowser) { filtered[name] = boundBrowser; hasAny = true; }
1282
+ continue;
1283
+ }
1266
1284
  if (name === "clay-documents" && forSession) {
1267
1285
  try {
1268
1286
  var boundDocuments = _sessionDocument.createMcpServer(adapter, forSession);
@@ -1374,6 +1392,7 @@ function createProjectContext(opts) {
1374
1392
  _sessionHandoff.getSystemPrompt(session),
1375
1393
  _driverContinuation.getSystemPrompt(session),
1376
1394
  _sessionDocument.getSystemPrompt(session),
1395
+ _sharedBrowser.getSystemPrompt(session),
1377
1396
  _capsuleCatalog.getSystemPrompt(session),
1378
1397
  _projectLogs.getSystemPrompt(session),
1379
1398
  _issues.getSystemPrompt(session),
@@ -1393,6 +1412,7 @@ function createProjectContext(opts) {
1393
1412
  .concat(_sessionHandoff.getToolDefs(session))
1394
1413
  .concat(_driverContinuation.getToolDefs(session, Number(session._sdkQueryGeneration || 0)))
1395
1414
  .concat(_sessionDocument.getToolDefs(session))
1415
+ .concat(_sharedBrowser.getToolDefs(session))
1396
1416
  .concat(_debateProposal.getToolDefs(session))
1397
1417
  .concat(_mateCreationProposal.getToolDefs(session))
1398
1418
  .concat(_workspaceQuery.getDynamicToolDefs(session))
@@ -1821,6 +1841,7 @@ function createProjectContext(opts) {
1821
1841
  if (_shellCommand.handleShellCommand(ws, msg)) return;
1822
1842
 
1823
1843
  // --- Vendor login flow (delegated to project-vendor-login.js) ---
1844
+ if (_sharedBrowser.handleMessage(ws, msg)) return;
1824
1845
  if (_vendorLogin.handleVendorLoginMessage(ws, msg)) return;
1825
1846
 
1826
1847
  // --- Notes, terminals, context, user message (delegated to project-user-message.js) ---
@@ -2204,6 +2225,8 @@ function createProjectContext(opts) {
2204
2225
 
2205
2226
  // --- Vendor login flow (delegated to project-vendor-login.js) ---
2206
2227
  var _vendorLogin = attachVendorLogin({
2228
+ requestAccess: _requestAccess,
2229
+ clients: clients,
2207
2230
  slug: slug,
2208
2231
  osUsers: osUsers,
2209
2232
  sm: sm,
@@ -2342,6 +2365,11 @@ function createProjectContext(opts) {
2342
2365
  inputSchema: normalizeToolSchema(continuationTools[cti].inputSchema),
2343
2366
  });
2344
2367
  }
2368
+ var sharedTools = _sharedBrowser.getToolDefs(boundSession);
2369
+ sharedTools.forEach(function (definition) {
2370
+ tools.push({ server: "clay-shared-browser", name: definition.name, description: definition.description,
2371
+ inputSchema: normalizeToolSchema(definition.inputSchema) });
2372
+ });
2345
2373
  var documentTools = _sessionDocument.getToolDefs(boundSession);
2346
2374
  for (var dti = 0; dti < documentTools.length; dti++) {
2347
2375
  tools.push({
@@ -2385,6 +2413,7 @@ function createProjectContext(opts) {
2385
2413
  var inAppNames = Object.keys(localMcp);
2386
2414
  for (var i = 0; i < inAppNames.length; i++) {
2387
2415
  if (inAppNames[i] === "clay-sessions" || inAppNames[i] === "clay-notes" || inAppNames[i] === "clay-handoff" || inAppNames[i] === "clay-documents") continue;
2416
+ if (inAppNames[i] === "clay-shared-browser") continue;
2388
2417
  // Debate tools were added above only when this request had an
2389
2418
  // exact session. Never duplicate them or advertise the static,
2390
2419
  // intentionally unbound proposal server.
@@ -2444,6 +2473,12 @@ function createProjectContext(opts) {
2444
2473
  }
2445
2474
  }
2446
2475
  }
2476
+ if (boundSession && serverName === "clay-shared-browser") {
2477
+ var sharedTools = _sharedBrowser.getToolDefs(boundSession);
2478
+ for (var bti = 0; bti < sharedTools.length; bti++) {
2479
+ if (sharedTools[bti].name === toolName) return Promise.resolve(sharedTools[bti].handler(args || {}));
2480
+ }
2481
+ }
2447
2482
  if (boundSession && serverName === "clay-documents") {
2448
2483
  var documentTools = _sessionDocument.getToolDefs(boundSession);
2449
2484
  for (var dti = 0; dti < documentTools.length; dti++) {
@@ -2482,7 +2517,7 @@ function createProjectContext(opts) {
2482
2517
  if (boundSession && serverName === "clay-knowledge") {
2483
2518
  return _mateKnowledge.callBridgeTool(boundSession, toolName, args);
2484
2519
  }
2485
- 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") {
2520
+ 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-issues" || serverName === "clay-logs" || serverName === "clay-knowledge") {
2486
2521
  return Promise.reject(new Error("Session-bound tool requires a valid Clay session: " + serverName + "/" + toolName));
2487
2522
  }
2488
2523
  if (sessionOnly) return Promise.reject(new Error("Session tool not found: " + serverName + "/" + toolName));
@@ -2600,6 +2635,8 @@ function createProjectContext(opts) {
2600
2635
 
2601
2636
  // --- Destroy ---
2602
2637
  function destroy() {
2638
+ _sharedBrowser.destroy();
2639
+ _vendorLogin.destroy();
2603
2640
  if (_scheduledMessages) _scheduledMessages.shutdown();
2604
2641
  if (_autonomousRun) _autonomousRun.shutdown();
2605
2642
  _splitGroups.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';
@@ -268,6 +269,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
268
269
  // --- Initialize store (single source of truth for client state) ---
269
270
  createStore({
270
271
  networkSettings: { loaded: false, busy: false, confirming: false, draft: "", saved: "", status: "", errorKind: "", requestId: 0 },
272
+ vendorLoginBrowser: null,
273
+ sharedBrowser: null,
274
+ sharedBrowsers: [],
275
+ mcpSkillsWorkbench: {},
276
+ sharedBrowserOpen: false,
277
+ sharedBrowserUi: {},
271
278
  // session / routing
272
279
  basePath: basePath,
273
280
  wsPath: wsPath,
@@ -1174,6 +1181,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1174
1181
  fileViewerEl: $("file-viewer"),
1175
1182
  });
1176
1183
  initGitPanel();
1184
+ initSharedBrowser();
1177
1185
  initWorkerPaneLock();
1178
1186
 
1179
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,51 @@
1
+ .mcp-skills-workbench { display:flex; flex-direction:column; flex-shrink:0; width:50%; max-width:720px; min-width:360px; height:calc(100% - 16px); align-self:center; margin:8px 8px 8px 10px; border:1px solid var(--border); border-radius:12px; background:var(--bg); overflow:hidden; }
2
+ .mcp-skills-workbench.hidden { display:none; }
3
+ .mcp-skills-workbench.workbench-wide { width:70%; max-width:1200px; }
4
+ .mcp-skills-workbench.panel-fullscreen { width:100%; max-width:none; min-width:0; height:100%; margin:0; border:0; border-radius:0; }
5
+ .mcp-skills-header { display:flex; align-items:center; justify-content:space-between; padding:12px 14px; border-bottom:1px solid var(--border); }
6
+ .mcp-skills-header strong,.mcp-skills-header>div { display:flex; align-items:center; gap:8px; font-size:12px; font-weight:600; }
7
+ .mcp-skills-header svg { width:15px; height:15px; }
8
+ .mcp-skills-header button { display:flex; padding:6px; background:transparent; border:0; border-radius:6px; color:var(--text-muted); cursor:pointer; }
9
+ .mcp-skills-header button:hover { background:var(--bg-alt); color:var(--text); }
10
+ .mcp-skills-tabs { display:flex; padding:0 16px; border-bottom:1px solid var(--border); gap:24px; flex-shrink:0; }
11
+ .mcp-skills-tabs button { padding:13px 0 11px; background:none; border:0; border-bottom:2px solid transparent; color:var(--text-muted); font:inherit; font-size:12px; cursor:pointer; }
12
+ .mcp-skills-tabs button[aria-selected="true"] { color:var(--text); border-bottom-color:var(--accent); }
13
+ .mcp-skills-tabs button[hidden] { display:none; }
14
+ #mcp-skills-workbench #skills-modal,#mcp-skills-workbench #mcp-modal { position:static; inset:auto; z-index:auto; display:flex; flex:1; min-height:0; align-items:stretch; justify-content:stretch; }
15
+ #mcp-skills-workbench #skills-modal.hidden,#mcp-skills-workbench #mcp-modal.hidden { display:none; }
16
+ #mcp-skills-workbench .skills-dialog,#mcp-skills-workbench .mcp-dialog { width:100%; max-width:none; min-width:0; height:100%; max-height:none; padding:0; display:flex; flex-direction:column; overflow:hidden; }
17
+ #mcp-skills-workbench .mcp-header,#mcp-skills-workbench #skills-modal-close { display:none; }
18
+ #mcp-skills-workbench .skills-header { padding:12px 16px 0; }
19
+ #mcp-skills-workbench .skills-title { font-size:12px; font-weight:500; }
20
+ #mcp-skills-workbench .skills-powered-by { font-size:10px; }
21
+ #mcp-skills-workbench .skills-search { margin:12px 16px 0; }
22
+ #mcp-skills-workbench .skills-tabs { padding:8px 16px 0; gap:0; overflow-x:auto; }
23
+ #mcp-skills-workbench .skills-tab { padding:9px 10px; font-size:11px; white-space:nowrap; }
24
+ #mcp-skills-workbench .skills-content,#mcp-skills-workbench .mcp-content { flex:1; min-height:0; overflow-y:auto; padding:16px; }
25
+ #mcp-skills-workbench .skills-detail { display:flex; flex-direction:column; gap:24px; height:auto; min-height:0; }
26
+ #mcp-skills-workbench .skills-detail-main { min-width:0; flex:none; border-right:0; overflow:visible; }
27
+ #mcp-skills-workbench .skills-detail-sidebar { width:auto; border-left:0; padding-left:0; border-top:1px solid var(--border); padding-top:20px; }
28
+ #mcp-skills-workbench .skills-detail-name { font-size:24px; overflow-wrap:anywhere; }
29
+ #mcp-skills-workbench .skills-installed-item { align-items:flex-start; }
30
+ #mcp-skills-workbench .skills-item { padding:12px 0; }
31
+ #mcp-skills-workbench .mcp-server-row { flex-wrap:wrap; }
32
+ #mcp-skills-workbench .mcp-server-perm-reason { flex-basis:100%; padding-left:26px; }
33
+ #mcp-skills-workbench button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
34
+ @media (max-width:1023px) { .mcp-skills-workbench,.mcp-skills-workbench.workbench-wide { position:fixed; inset:0; z-index:160; width:100%; max-width:none; min-width:0; height:100dvh; margin:0; border:0; border-radius:0; padding-top:env(safe-area-inset-top); padding-bottom:env(safe-area-inset-bottom); } }
35
+
36
+ #mcp-skills-workbench .skills-detail-main > .skills-detail-md-wrap { flex:none; overflow:visible; padding:0; }
37
+ #mcp-skills-workbench .skills-detail-main > .skills-detail-name,#mcp-skills-workbench .skills-detail-main > .skills-detail-desc,#mcp-skills-workbench .skills-detail-main > .skills-detail-cmd { margin-left:0; margin-right:0; }
38
+
39
+ #mcp-skills-workbench .skills-detail-cmd code { min-width:0; overflow-x:auto; }
40
+
41
+ #mcp-skills-workbench .ext-pill-wrap { display:block; flex-shrink:0; padding:12px 16px; border-bottom:1px solid var(--border); max-height:55%; overflow-y:auto; }
42
+ #mcp-skills-workbench .top-bar-ext-btn { padding:8px 10px; border-radius:6px; gap:6px; }
43
+ #mcp-skills-workbench .top-bar-ext-btn::after { content:"Setup"; margin-left:8px; color:var(--text-muted); font-weight:400; }
44
+ #mcp-skills-workbench .top-bar-ext-btn.ext-connected::after { content:"Connected"; color:var(--success); }
45
+ #mcp-skills-workbench .ext-popover { position:static; width:auto; margin-top:12px; padding:12px; box-shadow:none; border-radius:8px; }
46
+
47
+ #mcp-skills-workbench .ext-feature-summary { margin:8px 0 0; font-size:12px; line-height:1.5; color:var(--text-muted); }
48
+ #mcp-skills-workbench .ext-popover-desc p { margin:0 0 8px; }
49
+ #mcp-skills-workbench .ext-feature-list { margin:0 0 16px; padding-left:18px; }
50
+ #mcp-skills-workbench .ext-feature-list li { margin-bottom:8px; line-height:1.6; }
51
+ #mcp-skills-workbench .ext-feature-list strong { color:var(--text); font-weight:500; }
@@ -18,12 +18,6 @@
18
18
  flex-direction: column;
19
19
  overflow: hidden;
20
20
  flex-shrink: 0;
21
- box-shadow:
22
- 4px 8px 18px rgba(var(--shadow-rgb), 0.14),
23
- 2px 2px 4px rgba(var(--shadow-rgb), 0.08),
24
- inset 0 1px 0 rgba(255, 255, 255, 0.04);
25
- animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
26
- transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
27
21
  }
28
22
  #project-logs-panel.hidden { display: none; }
29
23
  #project-logs-panel.project-logs-wide {
@@ -38,14 +32,9 @@
38
32
  margin: 0;
39
33
  border: 0;
40
34
  border-radius: 0;
41
- box-shadow: none;
42
- animation: none;
43
35
  }
44
36
  }
45
37
 
46
- @media (prefers-reduced-motion: reduce) {
47
- #project-logs-panel { animation: none; }
48
- }
49
38
 
50
39
  /* Narrow screens: full overlay, like the other workbench tools. */
51
40
  @media (max-width: 1023px) {
@@ -0,0 +1,28 @@
1
+ /* Shared desktop workbench elevation and motion. Keep panel content styles local. */
2
+ @keyframes workbench-panel-in {
3
+ from { opacity: 0; transform: translateX(10px) scale(0.995); }
4
+ to { opacity: 1; transform: translateX(0) scale(1); }
5
+ }
6
+
7
+ @media (min-width: 1024px) {
8
+ :is(#file-viewer, #terminal-container, #project-logs-panel, #issues-panel, #notes-browser, #scheduled-tasks-panel, #shared-browser-panel, #mcp-skills-workbench):not(.panel-fullscreen) {
9
+ box-shadow:
10
+ 4px 8px 18px rgba(var(--shadow-rgb), 0.14),
11
+ 2px 2px 4px rgba(var(--shadow-rgb), 0.08),
12
+ inset 0 1px 0 rgba(255, 255, 255, 0.04);
13
+ animation: workbench-panel-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
14
+ transition: max-width 0.2s ease, width 0.2s ease, box-shadow 0.2s ease;
15
+ }
16
+ }
17
+
18
+ :is(#file-viewer, #terminal-container, #project-logs-panel, #issues-panel, #notes-browser, #scheduled-tasks-panel, #shared-browser-panel, #mcp-skills-workbench).panel-fullscreen {
19
+ box-shadow: none;
20
+ animation: none;
21
+ }
22
+
23
+ @media (prefers-reduced-motion: reduce) {
24
+ :is(#file-viewer, #terminal-container, #project-logs-panel, #issues-panel, #notes-browser, #scheduled-tasks-panel, #shared-browser-panel, #mcp-skills-workbench):not(.panel-fullscreen) {
25
+ animation: none;
26
+ transition: none;
27
+ }
28
+ }
@@ -1,5 +1,5 @@
1
1
  @media (min-width: 1024px) {
2
- #scheduled-tasks-panel { width: 50%; max-width: 760px; min-width: 380px; height: calc(100% - 16px); min-height: 0; align-self: center; margin: 8px 8px 8px 10px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); display: flex; flex-direction: column; overflow: hidden; flex-shrink: 0; box-shadow: 4px 8px 18px rgba(var(--shadow-rgb), .14), 2px 2px 4px rgba(var(--shadow-rgb), .08); animation: workbench-panel-in .18s cubic-bezier(.2,.8,.2,1); }
2
+ #scheduled-tasks-panel { width: 50%; max-width: 760px; min-width: 380px; height: calc(100% - 16px); min-height: 0; align-self: center; margin: 8px 8px 8px 10px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); display: flex; flex-direction: column; overflow: hidden; flex-shrink: 0; }
3
3
  #scheduled-tasks-panel.hidden { display: none; }
4
4
  #scheduled-tasks-panel.scheduled-tasks-wide { width: 70%; max-width: 1200px; }
5
5
  #scheduled-tasks-panel.panel-fullscreen { width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; }
@@ -0,0 +1,28 @@
1
+ /* Device silhouettes follow viewport dimensions; they do not emulate an OS. */
2
+ .shared-browser-viewport[data-skin="laptop"] .shared-browser-monitor { border: 9px solid #252729; border-bottom: 30px solid #c8cbcc; border-radius: 12px 12px 5px 5px; box-shadow: 0 0 0 2px #acb0b2, 0 20px 32px #0005; }
3
+ .shared-browser-viewport[data-skin="laptop"] .shared-browser-monitor-brand { left: 0; right: 0; text-align: center; color: #53585a; }
4
+ .shared-browser-viewport[data-skin="laptop"] .shared-browser-monitor-stand { bottom: -49px; height: 18px; width: calc(100% + 60px); background: linear-gradient(#dfe1e2, #babfc2); border-radius: 2px 2px 45% 45%; box-shadow: 0 3px 0 #858b8e, 0 12px 15px #0005; }
5
+ .shared-browser-viewport[data-skin="laptop"] .shared-browser-monitor-stand::before { top: 0; bottom: auto; height: 5px; left: 42%; right: 42%; clip-path: none; background: #9ba1a4; border-radius: 0 0 5px 5px; }
6
+ .shared-browser-viewport[data-skin="laptop"] .shared-browser-monitor-stand::after { display: none; }
7
+ .shared-browser-viewport[data-skin="crt"] { background: #183638; }
8
+ .shared-browser-viewport[data-skin="crt"] .shared-browser-monitor { border: 22px solid #c9c7b9; border-bottom-width: 42px; border-radius: 24px; box-shadow: inset 0 0 0 3px #797b72, 0 0 0 2px #e4e1d4, 8px 10px 0 #999b90, 12px 24px 30px #0005; }
9
+ .shared-browser-viewport[data-skin="crt"] .shared-browser-monitor::before { inset: -5px; border: 5px solid; border-color: #8a8b80 #b4b5a8 #e3e1d4 #9c9e91; border-radius: 6px; }
10
+ .shared-browser-viewport[data-skin="crt"] .shared-browser-monitor-brand { bottom: -33px; }
11
+ .shared-browser-viewport[data-skin="crt"] .shared-browser-monitor::after { bottom: -28px; right: 6px; width: 9px; height: 9px; border: 3px solid #afb1a3; }
12
+ .shared-browser-viewport[data-skin="crt"] .shared-browser-monitor-stand { bottom: -66px; width: 35%; height: 24px; }
13
+ .shared-browser-viewport[data-skin="crt"] .shared-browser-monitor-detail { position: absolute; bottom: -27px; right: 35px; width: 40px; height: 4px; background: repeating-linear-gradient(90deg, #85897e 0 6px, transparent 6px 12px); }
14
+ .shared-browser-viewport[data-skin="mac"] { background: #292824; gap: 28px; }
15
+ .shared-browser-viewport[data-skin="mac"] .shared-browser-monitor { border: 24px solid #d7cfbb; border-top-width: 30px; border-bottom-width: 78px; border-radius: 15px 15px 7px 7px; box-shadow: 0 5px 0 #afa68f, 0 7px 0 #e6ddc9, 0 22px 30px #0005; }
16
+ .shared-browser-viewport[data-skin="mac"] .shared-browser-monitor::before { inset: -6px; border: 6px solid; border-color: #aaa18c #bcb39d #ede4cf #bab099; border-radius: 9px; }
17
+ .shared-browser-viewport[data-skin="mac"] .shared-browser-monitor-brand { bottom: -51px; left: 1px; color: #696250; }
18
+ .shared-browser-viewport[data-skin="mac"] .shared-browser-monitor-stand { display: none; }
19
+ .shared-browser-viewport[data-skin="mac"] .shared-browser-monitor-detail { position: absolute; bottom: -43px; right: 2px; width: 30%; height: 5px; background: #625f54; border-bottom: 2px solid #efe6d3; border-radius: 1px; }
20
+ .shared-browser-viewport[data-skin="mac"] .shared-browser-monitor-detail::after { content: ""; position: absolute; top: 15px; right: 0; width: 65%; height: 10px; background: repeating-linear-gradient(90deg, #aaa08a 0 2px, transparent 2px 5px); }
21
+ .shared-browser-viewport[data-skin="mac"] .shared-browser-monitor::after { bottom: -67px; }
22
+ .shared-browser-viewport[data-skin="phone"] { background: #23282d; gap: 24px; }
23
+ .shared-browser-viewport[data-skin="phone"] .shared-browser-monitor { border: 12px solid #141517; border-top-width: 48px; border-bottom-width: 54px; border-radius: 38px; box-shadow: 0 0 0 2px #9a9d9f, 0 0 0 4px #46494c, 0 20px 35px #0006; }
24
+ .shared-browser-viewport[data-skin="phone"] .shared-browser-monitor::before { inset: auto auto -44px 50%; width: 32px; height: 32px; transform: translateX(-50%); border: 1px solid #666a6e; border-radius: 50%; background: #191b1d; box-shadow: inset 0 0 0 2px #111; }
25
+ .shared-browser-viewport[data-skin="phone"] .shared-browser-monitor::after { top: -27px; bottom: auto; right: calc(50% + 39px); width: 6px; height: 6px; background: #283d52; box-shadow: 0 0 0 2px #08090a; }
26
+ .shared-browser-viewport[data-skin="phone"] .shared-browser-monitor-detail { position: absolute; top: -26px; left: 50%; transform: translateX(-50%); width: 42px; height: 5px; border-radius: 4px; background: #08090a; box-shadow: 0 1px 0 #383b3d; }
27
+ .shared-browser-viewport[data-skin="phone"] .shared-browser-monitor-brand { left: auto; right: 10px; bottom: -34px; font-size: 10px; color: #92979c; }
28
+ .shared-browser-viewport[data-skin="phone"] .shared-browser-monitor-stand { display: none; }