clay-server 5.3.1-beta.1 → 5.3.1-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 (65) hide show
  1. package/lib/contextual-screen-snapshot.js +13 -4
  2. package/lib/project-shared-browser.js +69 -21
  3. package/lib/project.js +2 -0
  4. package/lib/public/css/issues.css +1 -1
  5. package/lib/public/css/linear.css +7 -24
  6. package/lib/public/css/menus.css +2 -2
  7. package/lib/public/css/notifications-center.css +2 -2
  8. package/lib/public/css/split-view-layout.css +0 -16
  9. package/lib/public/css/tool-pane.css +12 -0
  10. package/lib/public/modules/contextual-clay-session.js +2 -2
  11. package/lib/public/modules/contextual-mate-context.js +7 -3
  12. package/lib/public/modules/contextual-view-metadata.js +23 -0
  13. package/lib/public/modules/linear-panel.js +5 -5
  14. package/lib/public/modules/pane-activation.js +5 -1
  15. package/lib/public/modules/shared-browser-tabs.js +4 -2
  16. package/lib/public/modules/split-view-presentations.js +3 -1
  17. package/lib/public/modules/split-view-worker-scroll.js +1 -1
  18. package/lib/public/modules/split-view-workers.js +20 -51
  19. package/lib/public/modules/tool-pane-chrome.js +3 -0
  20. package/lib/public/modules/tool-pane-drag.js +1 -1
  21. package/lib/public/modules/tool-pane-layout.js +4 -3
  22. package/lib/public/modules/workbench-window-controls.js +39 -0
  23. package/lib/public/pane-assets/{app-DR227IBJ.js → app-BDYH6Y7R.js} +1 -1
  24. package/lib/public/pane-assets/{chunk-UQPN3SQX.js → chunk-42C2CZEP.js} +1 -1
  25. package/lib/public/pane-assets/chunk-46OOGVHU.js +1 -0
  26. package/lib/public/pane-assets/chunk-4OJW4FF4.js +1 -0
  27. package/lib/public/pane-assets/chunk-E6NRPGVH.js +2 -0
  28. package/lib/public/pane-assets/chunk-F7VIE6FJ.js +1 -0
  29. package/lib/public/pane-assets/chunk-FCJYJLVZ.js +1 -0
  30. package/lib/public/pane-assets/chunk-FEJDEXOW.js +1 -0
  31. package/lib/public/pane-assets/chunk-FQQMXMH7.js +168 -0
  32. package/lib/public/pane-assets/chunk-HEMCIILG.js +1 -0
  33. package/lib/public/pane-assets/chunk-HWLBH6AL.js +1 -0
  34. package/lib/public/pane-assets/{chunk-KWUKYV6X.js → chunk-KB4T5B2R.js} +1 -1
  35. package/lib/public/pane-assets/chunk-KF3LZRJK.js +1 -0
  36. package/lib/public/pane-assets/{chunk-MLNNDUPE.js → chunk-N5SIEIOG.js} +1 -1
  37. package/lib/public/pane-assets/{chunk-YU4R2HSO.js → chunk-QRVKAVH7.js} +1 -1
  38. package/lib/public/pane-assets/chunk-RV5O66TO.js +1 -0
  39. package/lib/public/pane-assets/{chunk-4KFYASLC.js → chunk-TMUCEZ7C.js} +1 -1
  40. package/lib/public/pane-assets/{chunk-3CQA7ZCS.js → chunk-TT3NECKK.js} +1 -1
  41. package/lib/public/pane-assets/{chunk-FK43VMMW.js → chunk-U5GH2UJM.js} +1 -1
  42. package/lib/public/pane-assets/{chunk-5P3D2RTQ.js → chunk-UMIYQYGM.js} +1 -1
  43. package/lib/public/pane-assets/chunk-VCNLKOVT.js +1 -0
  44. package/lib/public/pane-assets/{chunk-3VNXCTTY.js → chunk-X452YCE5.js} +1 -1
  45. package/lib/public/pane-assets/{chunk-LLBLEJM4.js → chunk-XI2DJM4H.js} +1 -1
  46. package/lib/public/pane-assets/chunk-YE45RR7W.js +1 -0
  47. package/lib/public/pane-assets/manifest.json +390 -388
  48. package/lib/public/pane-assets/style-VDIGFFLR.css +1 -0
  49. package/lib/shared-browser-workspace.js +28 -0
  50. package/lib/workbench-window-layout.js +7 -5
  51. package/package.json +1 -1
  52. package/lib/public/pane-assets/chunk-3SZABBN7.js +0 -1
  53. package/lib/public/pane-assets/chunk-A3MDESCE.js +0 -1
  54. package/lib/public/pane-assets/chunk-G6ZL6VPR.js +0 -2
  55. package/lib/public/pane-assets/chunk-GMASVOWL.js +0 -1
  56. package/lib/public/pane-assets/chunk-HQUGZW4P.js +0 -1
  57. package/lib/public/pane-assets/chunk-LA7KKFI2.js +0 -1
  58. package/lib/public/pane-assets/chunk-MOSMN3CU.js +0 -1
  59. package/lib/public/pane-assets/chunk-MUAMABNY.js +0 -1
  60. package/lib/public/pane-assets/chunk-TABMZ63N.js +0 -1
  61. package/lib/public/pane-assets/chunk-TEHDJOM4.js +0 -1
  62. package/lib/public/pane-assets/chunk-UNTZB4WP.js +0 -166
  63. package/lib/public/pane-assets/chunk-WQE7BLFG.js +0 -1
  64. package/lib/public/pane-assets/chunk-XFUKDMBX.js +0 -1
  65. package/lib/public/pane-assets/style-BSKKK54Q.css +0 -1
@@ -1,7 +1,7 @@
1
1
  // Bounded display metadata supplied by the viewer. Never an authority token.
2
2
  var provenance = require('./session-provenance');
3
3
  var normalizeClientMessageId = require('./project-message-delivery').normalizeClientMessageId;
4
- var fields = { version: true, viewId: true, projectTitle: true, conversationTitle: true, projectSlug: true, sessionId: true, screen: true };
4
+ var fields = { version: true, viewId: true, projectTitle: true, conversationTitle: true, projectSlug: true, sessionId: true, screen: true, workspace: true };
5
5
  function validate(value) {
6
6
  if (!value || typeof value !== 'object' || Array.isArray(value) || value.version !== 1 || Object.keys(value).some(function (key) { return !Object.prototype.hasOwnProperty.call(fields, key); })) throw new Error('Invalid contextual snapshot.');
7
7
  if (typeof value.viewId !== 'string' || !/^context-[A-Za-z0-9-]{1,90}$/.test(value.viewId)) throw new Error('Invalid snapshot view identity.');
@@ -10,7 +10,16 @@ function validate(value) {
10
10
  });
11
11
  if (value.sessionId !== null && (!Number.isSafeInteger(value.sessionId) || value.sessionId < 1)) throw new Error('Invalid snapshot conversation.');
12
12
  if (['Home', 'Project', 'Direct message'].indexOf(value.screen) < 0) throw new Error('Invalid snapshot screen.');
13
- return Object.assign({}, value);
13
+ if (value.workspace != null) validateWorkspace(value.workspace);
14
+ return JSON.parse(JSON.stringify(value));
15
+ }
16
+ function validateWorkspace(value) {
17
+ function exact(item, keys) { return item && typeof item === 'object' && !Array.isArray(item) && Object.keys(item).length === keys.length && keys.every(function (key) { return Object.prototype.hasOwnProperty.call(item, key); }); }
18
+ function label(value) { return value === null || typeof value === 'string' && value.length <= 160 && !/[\u0000-\u001f\u007f]/.test(value); }
19
+ if (!exact(value, ['name', 'sessions', 'workbenches']) || !label(value.name) || !Array.isArray(value.sessions) || value.sessions.length > 32 || !Array.isArray(value.workbenches) || value.workbenches.length > 16 ||
20
+ !value.workbenches.every(function (name) { return typeof name === 'string' && label(name); }) || !value.sessions.every(function (session) {
21
+ return exact(session, ['sessionId', 'title', 'selected', 'focused']) && Number.isSafeInteger(session.sessionId) && session.sessionId > 0 && label(session.title) && typeof session.selected === 'boolean' && typeof session.focused === 'boolean';
22
+ })) throw new Error('Invalid contextual workspace metadata.');
14
23
  }
15
24
  function attachContextualScreenSnapshot(ctx) {
16
25
  function capture(ws, session, msg) {
@@ -19,11 +28,11 @@ function attachContextualScreenSnapshot(ctx) {
19
28
  function assertLive() {
20
29
  var owner = ctx.getProjectOwnerId(), actor = ws && ws._clayUser;
21
30
  var mate = ctx.isMate && ctx.matesModule.getMate(ctx.matesModule.buildMateCtx(owner), ctx.mateId);
22
- if (!mate || mate.builtinKey !== 'clay' || mate.archived || mate.lifecycleDeletion || ctx.sm.sessions.get(session.localId) !== session || session.hidden || session.destroying || session.spawn || provenance.isWorker(session) || session.delegated || session._delegatedBy || session._pairDelegation || session._mcpForeignInput || session._mentionInProgress || session.homeDebatePlanning || session.debateSetupMode || session.mateCreationMode || session.sessionVisibility === 'shared') throw new Error('Snapshots require a private canonical Clay conversation.');
31
+ if (!mate || mate.archived || mate.lifecycleDeletion || ctx.sm.sessions.get(session.localId) !== session || session.hidden || session.destroying || session.spawn || provenance.isWorker(session) || session.delegated || session._delegatedBy || session._pairDelegation || session._mcpForeignInput || session._mentionInProgress || session.homeDebatePlanning || session.debateSetupMode || session.mateCreationMode || session.sessionVisibility === 'shared') throw new Error('Snapshots require a private owner Mate conversation.');
23
32
  if (ctx.usersModule.isMultiUser() && (!actor || actor.id !== owner || session.ownerId !== owner || mate.createdBy !== owner || !ctx.usersModule.findUserById(owner) || !ctx.usersModule.canAccessSession(owner, session, ctx.getProjectAccess()))) throw new Error('Snapshot ownership is unavailable.');
24
33
  if (!ctx.usersModule.isMultiUser() && (session.ownerId && session.ownerId !== owner || actor && actor.id !== owner && actor.id !== 'default')) throw new Error('Snapshot ownership changed.');
25
34
  if (!normalizeClientMessageId(msg.clientMessageId) || !msg.sessionOriginId || msg.sessionOriginId !== session.sessionOriginId || String(msg.sessionId) !== String(session.localId) || msg.projectSlug !== ctx.slug || msg.accountId !== (actor && actor.id || '_default')) throw new Error('Snapshot delivery identity changed.');
26
- var current = JSON.stringify([mate.id, mate.createdAt, mate.createdBy, session.sessionOriginId]);
35
+ var current = JSON.stringify([mate.id, mate.builtinKey, mate.createdAt, mate.createdBy, session.sessionOriginId]);
27
36
  if (stamp && stamp !== current) throw new Error('Snapshot source identity changed.');
28
37
  stamp = current;
29
38
  }
@@ -3,6 +3,7 @@ var crypto = require("crypto");
3
3
  var policy = require("./shared-browser-policy");
4
4
  var browserTools = require("./shared-browser-tools");
5
5
  var createRuntime = require("./shared-browser-runtime").createRuntime;
6
+ var workspace = require('./shared-browser-workspace');
6
7
 
7
8
  function attachSharedBrowser(ctx) {
8
9
  var storageStore = ctx.storageStore || require("./shared-browser-storage").attachStorageStore(require("./config").CONFIG_DIR);
@@ -28,7 +29,7 @@ function attachSharedBrowser(ctx) {
28
29
  return identity ? JSON.stringify([identity.uid, identity.gid, identity.home]) : "default";
29
30
  }
30
31
  function recordAllowed(record, ws) {
31
- try { return allowed(record.session, ws) && record.ownerId === record.session.ownerId && record.identity === identityKey(record.session); }
32
+ try { return allowed(record.session, ws) && record.ownerId === record.session.ownerId && record.identity === identityKey(record.session) && record.sourceStamp === workspace.sourceStamp(record.session); }
32
33
  catch (e) { return false; }
33
34
  }
34
35
  function requireAccess(session, ws) {
@@ -42,11 +43,21 @@ function attachSharedBrowser(ctx) {
42
43
  function sessionRecords(session) {
43
44
  return Array.from(records.values()).filter(function (record) { return record.session === session && recordAllowed(record); });
44
45
  }
45
- function selectRecord(session, id) {
46
- var tabs = sessionRecords(session);
46
+ function humanAllowed(session, record, ws) {
47
+ return allowed(session, ws) && recordAllowed(record, ws) &&
48
+ (record.session === session || workspace.linkedWorker(ctx, session, record.session));
49
+ }
50
+ function humanRecords(session, ws) {
51
+ return Array.from(records.values()).filter(function (record) { return humanAllowed(session, record, ws); });
52
+ }
53
+ function requireHumanRecord(session, record, ws) {
54
+ if (ctx.getSessionForWs(ws) !== session || !humanAllowed(session, record, ws)) throw new Error('Browser session expired');
55
+ }
56
+ function selectRecord(session, id, ws) {
57
+ var tabs = ws ? humanRecords(session, ws) : sessionRecords(session);
47
58
  if (id) {
48
59
  var record = records.get(id);
49
- if (!record || record.session !== session || !recordAllowed(record)) throw new Error("Browser tab expired");
60
+ if (!record || tabs.indexOf(record) < 0) throw new Error("Browser tab expired");
50
61
  return record;
51
62
  }
52
63
  if (tabs.length > 1) throw new Error("Choose a browserId from status before operating a tab");
@@ -57,21 +68,28 @@ function attachSharedBrowser(ctx) {
57
68
  if (payload) ctx.sendTo(ws, payload);
58
69
  }
59
70
  function sendState(ws, session, record, present, focus) {
60
- if (!ws || ws.readyState !== 1 || !allowed(session, ws) || (record && !recordAllowed(record, ws))) return;
61
- sendBrowserMessage(ws, session, { type: "shared_browser_state", sessionId: session.localId, browser: projection(record), browsers: sessionRecords(session).map(projection), present: !!present, focus: !!focus });
71
+ if (!ws || ws.readyState !== 1 || !allowed(session, ws)) return;
72
+ var tabs = humanRecords(session, ws);
73
+ if (!record || tabs.indexOf(record) < 0) record = tabs[0] || null;
74
+ function humanProjection(tab) { return Object.assign({}, projection(tab), workspace.describe(tab)); }
75
+ sendBrowserMessage(ws, session, { type: "shared_browser_state", sessionId: session.localId, browser: record ? humanProjection(record) : null,
76
+ browsers: tabs.map(humanProjection), present: !!present && record && record.session === session, focus: !!focus });
62
77
  }
63
78
  function broadcast(record, present) {
64
79
  ctx.clients.forEach(function (ws) {
65
- if (ctx.getSessionForWs(ws) === record.session) sendState(ws, record.session, record, present);
80
+ var session = ctx.getSessionForWs(ws);
81
+ if (session && (session.ownerId || null) === (record.ownerId || null)) sendState(ws, session, record, present);
66
82
  });
67
83
  }
68
84
  function watching(record, ws) {
69
- return ws.readyState === 1 && viewers.get(ws) === record.id && ctx.getSessionForWs(ws) === record.session && recordAllowed(record, ws);
85
+ var viewer = viewers.get(ws);
86
+ return ws.readyState === 1 && viewer && viewer.id === record.id && ctx.getSessionForWs(ws) === viewer.session &&
87
+ viewer.sourceStamp === workspace.sourceStamp(viewer.session) && humanAllowed(viewer.session, record, ws);
70
88
  }
71
89
  function visibility(record) {
72
90
  if (!record.runtime) return;
73
91
  var visible = false;
74
- viewers.forEach(function (id, ws) { if (id === record.id && watching(record, ws)) visible = true; });
92
+ viewers.forEach(function (viewer, ws) { if (viewer.id === record.id && watching(record, ws)) visible = true; });
75
93
  record.runtime.viewing(visible);
76
94
  }
77
95
  function end(record, reason) {
@@ -114,7 +132,10 @@ function attachSharedBrowser(ctx) {
114
132
  record.frame = { type: "shared_browser_frame", sessionId: record.session.localId, browserId: record.id,
115
133
  data: message.data, width: record.width, height: record.height };
116
134
  viewers.forEach(function (id, ws) {
117
- if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) sendBrowserMessage(ws, record.session, record.frame);
135
+ if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) {
136
+ var session = viewers.get(ws).session;
137
+ sendBrowserMessage(ws, session, Object.assign({}, record.frame, { sessionId: session.localId }));
138
+ }
118
139
  });
119
140
  visibility(record);
120
141
  } else broadcast(record);
@@ -136,7 +157,7 @@ function attachSharedBrowser(ctx) {
136
157
  var active = 0;
137
158
  records.forEach(function (record) { if (record.runtime && record.ownerId === session.ownerId) active++; });
138
159
  if (active >= 4) throw new Error("End another browser before opening more than four active tabs");
139
- var record = { session: session, ownerId: session.ownerId, identity: identityKey(session), id: crypto.randomUUID(), phase: "starting", control: "agent", epoch: 0,
160
+ var record = { session: session, sourceStamp: workspace.sourceStamp(session), ownerId: session.ownerId, identity: identityKey(session), id: crypto.randomUUID(), phase: "starting", control: "agent", epoch: 0,
140
161
  cookieOwner: ctx.isMate ? (ctx.ownerId || session.ownerId || "default") : null, incognito: temporary, url: "", width: 1280, height: 800, touched: Date.now(), runtime: null, frame: null };
141
162
  record.ready = new Promise(function (resolve, reject) { record.resolveReady = resolve; record.rejectReady = reject; });
142
163
  record.ready.catch(function () {});
@@ -150,7 +171,8 @@ function attachSharedBrowser(ctx) {
150
171
  broadcast(record, true);
151
172
  return record;
152
173
  }
153
- async function changeControl(record, control) {
174
+ async function changeControl(record, control, validate) {
175
+ if (validate) validate();
154
176
  if (!record.runtime) throw new Error("Open a browser first");
155
177
  record.control = control;
156
178
  activityHistory.interruptActivity(record); record.pointer = null;
@@ -161,8 +183,12 @@ function attachSharedBrowser(ctx) {
161
183
  broadcast(record);
162
184
  try {
163
185
  await record.runtime.control(epoch);
164
- if (record.runtime && record.epoch === epoch) { record.handoff = false; broadcast(record); }
165
186
  } catch (e) { if (record.runtime && record.epoch === epoch) end(record, e.message); }
187
+ if (record.runtime && record.epoch === epoch) {
188
+ record.handoff = false;
189
+ if (validate) validate();
190
+ broadcast(record);
191
+ }
166
192
 
167
193
  }
168
194
  // Finish is a serialized boundary: it waits for earlier queued work in the browser worker, then
@@ -179,7 +205,8 @@ function attachSharedBrowser(ctx) {
179
205
  record.touched = Date.now(); broadcast(record);
180
206
  return projection(record);
181
207
  }
182
- async function run(session, record, actor, action, event, intent, presentationStart) {
208
+ async function run(session, record, actor, action, event, intent, presentationStart, validate) {
209
+ if (validate) validate();
183
210
  requireAccess(session);
184
211
  if (!record || !record.runtime || !recordAllowed(record)) throw new Error("Open a browser first");
185
212
  if (record.handoff) throw new Error("Browser control is changing; wait for the handoff");
@@ -192,6 +219,7 @@ function attachSharedBrowser(ctx) {
192
219
  await record.ready;
193
220
  if (actor === "agent" && Number(session._sdkQueryGeneration || 0) !== generation) throw new Error("Browser request belongs to an older query");
194
221
  requireAccess(session);
222
+ if (validate) validate();
195
223
  if (!recordAllowed(record) || !record.runtime || record.epoch !== epoch) throw new Error("Browser control changed; retry after the handoff");
196
224
  record.touched = Date.now();
197
225
  var result;
@@ -203,6 +231,7 @@ function attachSharedBrowser(ctx) {
203
231
  throw error;
204
232
  }
205
233
  requireAccess(session);
234
+ if (validate) validate();
206
235
  if (actor === "agent" && Number(session._sdkQueryGeneration || 0) !== generation) throw new Error("Browser result belongs to an older query");
207
236
  if (!recordAllowed(record) || records.get(record.id) !== record || record.epoch !== epoch) throw new Error("Browser control changed during the action");
208
237
  if (result && result.url !== undefined) record.url = policy.visibleUrl(result.url);
@@ -268,7 +297,13 @@ function attachSharedBrowser(ctx) {
268
297
  function handleMessage(ws, msg) {
269
298
  if (!msg.type || msg.type.indexOf("shared_browser_") !== 0) return false;
270
299
  var session = ctx.getSessionForWs(ws);
300
+ var requestStamp = workspace.sourceStamp(session), requestBinding = ws._clayPaneActivation;
271
301
  Promise.resolve().then(async function () {
302
+ function validateRecord() {
303
+ if (requestStamp !== workspace.sourceStamp(session) || requestBinding !== ws._clayPaneActivation ||
304
+ requestBinding && (!ws._clayPaneActivationValid || !ws._clayPaneActivationValid())) throw new Error('Browser session changed');
305
+ requireHumanRecord(session, record, ws);
306
+ }
272
307
  requireAccess(session, ws);
273
308
  if (String(msg.sessionId) !== String(session.localId)) throw new Error("Browser session changed");
274
309
  var record;
@@ -285,26 +320,35 @@ function attachSharedBrowser(ctx) {
285
320
  if (msg.type === "shared_browser_open") {
286
321
  var targetUrl = msg.url === undefined ? null : policy.browserUrl(msg.url);
287
322
  if (targetUrl) record = await openLinkedPage(session, targetUrl, msg.incognito);
288
- else record = create(session, "user", msg.newTab === true, msg.incognito);
323
+ else {
324
+ var existing = msg.newTab !== true && msg.incognito === undefined && humanRecords(session, ws).filter(function (tab) { return !!tab.runtime; });
325
+ record = existing && (existing.find(function (tab) { return tab.session === session; }) || existing[0]) || create(session, "user", msg.newTab === true, msg.incognito);
326
+ }
289
327
  sendState(ws, session, record, true, true); return;
290
328
  }
291
- record = selectRecord(session, msg.browserId);
329
+ record = selectRecord(session, msg.browserId, ws);
292
330
  if (!record || !recordAllowed(record, ws) || msg.browserId !== record.id) throw new Error("Browser session expired");
331
+ validateRecord();
293
332
  if (msg.type === "shared_browser_view") {
294
- if (msg.visible) { var previous = records.get(viewers.get(ws)); viewers.set(ws, record.id); if (previous && previous !== record) visibility(previous); if (record.frame) sendBrowserMessage(ws, record.session, record.frame); }
333
+ if (msg.visible) {
334
+ var previousViewer = viewers.get(ws), previous = previousViewer && records.get(previousViewer.id);
335
+ viewers.set(ws, { id: record.id, session: session, sourceStamp: workspace.sourceStamp(session) });
336
+ if (previous && previous !== record) visibility(previous);
337
+ if (record.frame) sendBrowserMessage(ws, session, Object.assign({}, record.frame, { sessionId: session.localId }));
338
+ }
295
339
  else viewers.delete(ws);
296
340
  visibility(record);
297
341
  } else if (msg.type === "shared_browser_control") {
298
342
  if (!["agent", "user"].includes(msg.control)) throw new Error("Invalid control mode");
299
- await changeControl(record, msg.control);
343
+ await changeControl(record, msg.control, validateRecord);
300
344
  } else if (msg.type === "shared_browser_close") {
301
345
  end(record); records.delete(record.id);
302
346
  sendState(ws, session, sessionRecords(session)[0]);
303
- ctx.clients.forEach(function (peer) { if (peer !== ws && ctx.getSessionForWs(peer) === session) sendState(peer, session, sessionRecords(session)[0]); });
347
+ broadcast(record);
304
348
  } else if (msg.type === "shared_browser_end") end(record);
305
349
  else if (msg.type === "shared_browser_input") {
306
350
  if (msg.epoch !== record.epoch) throw new Error("Browser control changed");
307
- await run(session, record, "user", "action", policy.validateAction(msg.event));
351
+ await run(session, record, "user", "action", policy.validateAction(msg.event), null, null, validateRecord);
308
352
  }
309
353
  }).catch(function (e) {
310
354
  if (ws.readyState === 1) ctx.sendTo(ws, { type: "shared_browser_error", sessionId: msg.sessionId, error: e.message, requestId: msg.requestId });
@@ -312,7 +356,7 @@ function attachSharedBrowser(ctx) {
312
356
  return true;
313
357
  }
314
358
  var sweep = setInterval(function () {
315
- viewers.forEach(function (id, ws) { if (ws.readyState !== 1) viewers.delete(ws); });
359
+ viewers.forEach(function (viewer, ws) { var record = records.get(viewer.id); if (!record || !watching(record, ws)) viewers.delete(ws); });
316
360
  records.forEach(function (record, key) {
317
361
  if (!recordAllowed(record)) { end(record, "Browser session is no longer available"); records.delete(key); }
318
362
  else if (Date.now() - record.touched > 30 * 60 * 1000) { end(record, "Browser expired after 30 minutes without activity"); records.delete(key); }
@@ -330,6 +374,10 @@ function attachSharedBrowser(ctx) {
330
374
  });
331
375
  }
332
376
  return {
377
+ refresh: function () {
378
+ viewers.forEach(function (viewer, ws) { var record = records.get(viewer.id); if (!record || !watching(record, ws)) { viewers.delete(ws); if (record) visibility(record); } });
379
+ ctx.clients.forEach(function (ws) { var session = ctx.getSessionForWs(ws); if (session) sendState(ws, session); });
380
+ },
333
381
  handleMessage: handleMessage,
334
382
  getToolDefs: getToolDefs,
335
383
  getSystemPrompt: function () { return browserTools.PROMPT; },
package/lib/project.js CHANGED
@@ -692,6 +692,7 @@ function createProjectContext(opts) {
692
692
  usersModule: usersModule,
693
693
  multiWorkerFeature: multiWorkerGate,
694
694
  onPairChanged: function (group) {
695
+ if (_sharedBrowser) _sharedBrowser.refresh();
695
696
  if (!sdk || !group || !Array.isArray(group.members)) return;
696
697
  for (var i = 0; i < group.members.length; i++) {
697
698
  sdk.refreshSessionRuntime(sm.sessions.get(group.members[i]));
@@ -822,6 +823,7 @@ function createProjectContext(opts) {
822
823
  getSessionForWs: getSessionForWs,
823
824
  });
824
825
  var _sharedBrowser = attachSharedBrowser({
826
+ getWorkerGroup: function (id) { return _splitGroups.store.groupForMember(id); },
825
827
  studioPresentation: _studioPresentation,
826
828
  sendToSession: sendToSession,
827
829
  getPresentation: function (session) { return _workspaceQuery && _workspaceQuery.getPresentation(session); },
@@ -50,7 +50,7 @@
50
50
  .issue-evidence { padding-top: 18px; border-top: 1px solid var(--border-subtle); font-size: 13px; line-height: 1.65; }
51
51
  #issues-panel .issue-discussion { margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--border-subtle); }
52
52
  .issue-discussion h3 > span { margin-left: 4px; font: 11px var(--font-mono); color: var(--text-dimmer); }
53
- #issues-panel .issue-comment { padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--border-subtle); border-radius: 9px; background: var(--bg-alt); }
53
+ :is(#issues-panel, #linear-panel) .issue-comment { padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--border-subtle); border-radius: 9px; background: var(--bg-alt); }
54
54
  .issue-comment-meta { margin-bottom: 5px; color: var(--text-dimmer); font: 10.5px/1.4 var(--font-mono); }
55
55
  .issue-comment .markdown-body { font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
56
56
  .issue-comment .markdown-body > :first-child { margin-top: 0; }
@@ -1,7 +1,6 @@
1
- /* Linear workbench: mirrors #issues-panel / #terminal-container shell, header and controls. */
2
- #linear-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; }
1
+ /* Issue-specific content; the shared workbench shell owns window geometry. */
2
+ #linear-panel { min-height: 0; display: flex; flex-direction: column; background: var(--bg); color: var(--text); overflow: hidden; }
3
3
  #linear-panel.hidden { display: none; }
4
- #linear-panel .linear-topbar { 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; }
5
4
  #linear-panel .linear-title { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; flex: 1; }
6
5
 
7
6
  #linear-panel .linear-window-actions { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
@@ -9,8 +8,6 @@
9
8
  #linear-panel button:hover { background: var(--bg-hover, rgba(128,128,128,.12)); color: var(--text); }
10
9
  #linear-panel button:disabled { opacity: .5; cursor: default; }
11
10
  #linear-panel :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
12
- #linear-panel .linear-icon-btn { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border-radius: 7px; }
13
- #linear-panel .linear-icon-btn .lucide { display: block; width: 15px; height: 15px; }
14
11
  .linear-notice { margin: 0; padding: 0 16px; color: var(--text-muted); font-size: 12px; }
15
12
  .linear-notice:not(:empty) { padding: 8px 16px; }
16
13
  .linear-content { overflow: auto; min-height: 0; flex: 1; }
@@ -21,12 +18,9 @@
21
18
  .linear-actions button { font-size: 12px; line-height: normal; }
22
19
  #linear-panel a { color: var(--link); }
23
20
  #linear-panel .issue-row { width: 100%; text-align: left; }
24
- #linear-panel .issue-discussion article { padding: 14px 0; border-top: 1px solid var(--border-subtle); }
25
- #linear-panel .issue-discussion article > strong { display: block; margin-bottom: 4px; font-size: 13px; }
26
21
  #linear-panel .issue-doc-body, #linear-panel .issue-doc-body .md-content { overflow-wrap: anywhere; }
27
22
  #linear-panel .issue-doc-body .md-content { font-size: 14px; line-height: 1.7; }
28
23
  #linear-panel .issue-discussion .md-content { font-size: 13px; line-height: 1.6; }
29
- #linear-panel .issue-detail .issue-doc-title { font-size: clamp(20px, 2vw, 26px); line-height: 1.2; }
30
24
  #linear-panel .md-content > :first-child { margin-top: 0; }
31
25
  #linear-panel .md-content > :last-child { margin-bottom: 0; }
32
26
  #linear-panel .md-content h1, #linear-panel .md-content h2, #linear-panel .md-content h3 { margin: 1.3em 0 .5em; font-family: var(--font-display); }
@@ -36,7 +30,6 @@
36
30
  #linear-panel .md-content pre { overflow-x: auto; max-width: 100%; }
37
31
  #linear-panel .md-content table { display: block; max-width: 100%; overflow-x: auto; }
38
32
  #linear-panel .md-content img { max-width: 100%; }
39
- @media (max-width: 1023px) { #linear-panel { position: fixed; inset: 0; z-index: 300; width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; padding-top: var(--safe-top, 0px); padding-bottom: var(--safe-bottom, 0px); box-sizing: border-box; } #linear-panel button { min-height: 36px; } #linear-panel .linear-icon-btn { width: 36px; } }
40
33
  @media (max-width: 767px) { #linear-panel .issue-detail { padding: 20px 16px 40px; } }
41
34
  .session-linear-link { max-width: 360px; min-width: 0; flex-shrink: 1; }
42
35
  .session-linear-link > span { white-space: nowrap; }
@@ -49,22 +42,15 @@
49
42
  #header-github-links:has(.session-linear-link) { max-width: 170px; }
50
43
  #header-github-links .linear-work-title, #header-github-links .linear-work-state { display: none; }
51
44
  }
52
- #linear-panel .linear-session-pills { display: flex; flex-wrap: wrap; gap: 8px; }
53
- #linear-panel .linear-session-pill { display: inline-flex; align-items: flex-start; gap: 7px; max-width: 100%; box-sizing: border-box; padding: 6px 12px; border-radius: 999px; text-align: left; font-size: 13px; line-height: 1.4; color: var(--text); background: var(--bg-alt); border: 1px solid var(--border); }
54
- #linear-panel .linear-session-pill .lucide { width: 14px; height: 14px; flex: none; margin-top: 2px; color: var(--accent); }
55
- #linear-panel .linear-session-pill > span { min-width: 0; overflow-wrap: anywhere; }
56
- #linear-panel .linear-session-pill:hover { background: color-mix(in srgb, var(--accent) 10%, var(--bg-alt)); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
57
- #linear-panel .linear-session-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
58
45
  .linear-work-brand { display: inline-flex; align-items: center; flex: none; color: inherit; }
59
46
  .linear-work-brand svg { width: 13px; height: 13px; }
60
47
 
61
- #linear-panel .linear-title .linear-work-brand svg { width: 16px; height: 16px; }
48
+ #linear-panel .linear-title .linear-work-brand svg { width: 14px; height: 14px; }
62
49
  #linear-panel a.linear-icon-btn { color: var(--text-secondary); text-decoration: none; border: 1px solid var(--border); background: var(--bg-alt); box-sizing: border-box; }
63
50
  #linear-panel a.linear-icon-btn:hover { background: var(--bg-hover, rgba(128,128,128,.12)); color: var(--text); }
64
51
  #linear-panel [data-linear-external][hidden] { display: none; }
65
52
  #linear-panel .linear-conversations { margin: 22px 0 26px; }
66
53
  #linear-panel .linear-conversations h3 { font-size: 12px; font-weight: 600; color: var(--text-secondary); margin: 0 0 10px; }
67
- #linear-panel .linear-action-chip { background: transparent; border-style: dashed; color: var(--text-secondary); }
68
54
  #linear-panel .linear-sharing-hint { font-size: 11px; line-height: 1.5; color: var(--text-muted); margin: 10px 0 0; }
69
55
  @media (max-width: 1023px) { #linear-panel a.linear-icon-btn { height: 36px; } }
70
56
 
@@ -73,19 +59,16 @@
73
59
  #linear-panel .linear-issue-id { padding: 4px 7px; border-radius: 5px; background: var(--bg-alt); color: var(--text-secondary); font-family: var(--font-mono); font-weight: 500; letter-spacing: .015em; overflow-wrap: anywhere; }
74
60
  #linear-panel .linear-issue-status { padding: 3px 8px; border: 1px solid var(--border); border-radius: 999px; color: var(--text-secondary); overflow-wrap: anywhere; }
75
61
 
76
- #linear-panel .linear-title { min-width: 0; gap: 9px; height: 30px; font-size: 13px; line-height: 20px; font-weight: 600; }
62
+ #linear-panel .linear-title { min-width: 0; gap: 7px; height: auto; font-size: 11px; line-height: 20px; font-weight: 600; }
77
63
  #linear-panel .linear-window-actions { flex-shrink: 0; }
78
- #linear-panel .linear-header-issue { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; height: 30px; box-sizing: border-box; padding: 0 5px; border-radius: 5px; color: var(--text-secondary); text-decoration: none; font: inherit; }
64
+ #linear-panel .linear-header-issue { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; height: 24px; box-sizing: border-box; padding: 0 5px; border-radius: 5px; color: var(--text-secondary); text-decoration: none; font: inherit; }
79
65
  #linear-panel .linear-header-issue .lucide { display: block; width: 12px; height: 12px; color: inherit; }
80
66
  #linear-panel .linear-header-issue:hover { color: var(--text); background: var(--bg-alt); }
81
- #linear-panel .linear-window-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; line-height: 20px; font-weight: 600; }
67
+ #linear-panel .linear-window-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; line-height: 20px; font-weight: 500; }
82
68
 
83
- #linear-panel .issue-discussion .linear-comment { margin: 12px 0; padding: 16px; border: 1px solid var(--border-subtle); border-radius: 10px; background: var(--bg-alt); }
84
- #linear-panel .linear-comment > header { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; font-size: 13px; }
69
+ #linear-panel .linear-comment > header { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-bottom: 5px; }
85
70
  #linear-panel .linear-comment > header strong { font-weight: 600; }
86
- #linear-panel .linear-comment time { color: var(--text-muted); font-size: 11px; }
87
71
  #linear-panel .linear-comment .issue-doc-body { margin: 0; }
88
- @media (max-width: 767px) { #linear-panel .issue-discussion .linear-comment { padding: 12px; } }
89
72
 
90
73
  #linear-panel .linear-title > .linear-work-brand { height: 20px; align-items: center; }
91
74
  #linear-panel .linear-title > .linear-work-brand svg { display: block; }
@@ -250,8 +250,8 @@
250
250
  position: absolute;
251
251
  top: -4px;
252
252
  right: -4px;
253
- background: var(--success);
254
- color: #fff;
253
+ background: var(--brand-green);
254
+ color: var(--brand-ink);
255
255
  font-size: 9px;
256
256
  font-weight: 700;
257
257
  min-width: 14px;
@@ -11,8 +11,8 @@
11
11
  position: absolute;
12
12
  top: -3px;
13
13
  right: -3px;
14
- background: var(--error, #e53935);
15
- color: #fff;
14
+ background: var(--brand-coral);
15
+ color: var(--brand-ink);
16
16
  font-size: 9px;
17
17
  font-weight: 700;
18
18
  min-width: 15px;
@@ -258,22 +258,6 @@ body.split-view-resizing .split-pane-frame { pointer-events: none; }
258
258
  .split-view-worker-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
259
259
  .split-view-worker-identity { flex-shrink: 0; white-space: nowrap; }
260
260
  .split-view-worker-generation { flex-shrink: 0; color: var(--text-dimmer); white-space: nowrap; }
261
- .split-view-worker-controls {
262
- appearance: none;
263
- flex: 0 0 auto;
264
- height: 22px;
265
- margin-left: auto;
266
- padding: 3px 6px;
267
- border: 1px solid transparent;
268
- border-radius: 5px;
269
- background: transparent;
270
- color: var(--text-muted);
271
- font: inherit;
272
- font-size: 10px;
273
- cursor: pointer;
274
- }
275
- .split-view-worker-controls:hover { background: var(--sidebar-hover); color: var(--text); }
276
- .split-view-worker-controls:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
277
261
  .split-view-worker-dot { width: 5px; height: 5px; flex: 0 0 5px; border-radius: 50%; background: var(--text-dimmer); }
278
262
  .split-view-worker-dot[data-state="working"] { background: var(--brand-green); }
279
263
  .split-view-worker-dot[data-state="waiting"] { background: var(--brand-gold); }
@@ -122,3 +122,15 @@
122
122
  #main-panels > .workbench-pane-shell > .tool-pane-header .workbench-pane-actions button[aria-pressed="true"] { background: var(--accent-bg); color: var(--accent); }
123
123
 
124
124
  .file-instance:not([data-workbench-placement="docked"]) [data-action="pin"] { display: none; }
125
+
126
+ /* Related conversations and actions share the compact workbench control recipe. */
127
+ .workbench-related-links { display: flex; flex-wrap: wrap; gap: 6px; }
128
+ #main-panels .workbench-related-link { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; max-width: 100%; box-sizing: border-box; padding: 3px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-alt); color: var(--text-secondary); text-align: left; font-size: 12px; line-height: 20px; }
129
+ #main-panels .workbench-related-link > span { min-width: 0; overflow-wrap: anywhere; }
130
+ #main-panels .workbench-related-link svg { width: 13px; height: 13px; flex: none; }
131
+ #main-panels .workbench-related-link:hover { background: var(--sidebar-hover); color: var(--text); }
132
+ @media (max-width: 1023px) {
133
+ #main-panels > .workbench-pane-shell { position: fixed; inset: 0; z-index: 300; width: 100%; max-width: none; min-width: 0; height: 100%; margin: 0; border: 0; border-radius: 0; padding-top: var(--safe-top, 0px); padding-bottom: var(--safe-bottom, 0px); box-sizing: border-box; }
134
+ #main-panels > .workbench-pane-shell [data-workbench-window] { display: none; }
135
+ #main-panels .workbench-related-link { min-height: 36px; }
136
+ }
@@ -146,9 +146,9 @@ export function contextualScreenReference() {
146
146
  if (!enabled() || window.parent === window) return null;
147
147
  try {
148
148
  var surface = window.parent.document.getElementById('contextual-clay');
149
- if (!surface || !window.frameElement || window.frameElement.hidden || window.frameElement.dataset.canonical !== 'true' || !surface.contains(window.frameElement)) return null;
149
+ if (!surface || !window.frameElement || window.frameElement.hidden || !surface.contains(window.frameElement)) return null;
150
150
  var reference = JSON.parse(surface.dataset.screenReference || 'null');
151
151
  if (!reference || reference.ownerId !== (store.get('myUserId') || null)) return null;
152
- return { version: 1, viewId: typeof reference.viewId === 'string' ? reference.viewId.slice(0, 100) : null, projectTitle: typeof reference.projectTitle === 'string' ? reference.projectTitle.slice(0, 160) : null, conversationTitle: typeof reference.conversationTitle === 'string' ? reference.conversationTitle.slice(0, 160) : null, projectSlug: typeof reference.projectSlug === 'string' ? reference.projectSlug.slice(0, 160) : null, sessionId: Number.isSafeInteger(reference.sessionId) ? reference.sessionId : null, screen: String(reference.screen || '').slice(0, 40) };
152
+ return { version: 1, viewId: typeof reference.viewId === 'string' ? reference.viewId.slice(0, 100) : null, projectTitle: typeof reference.projectTitle === 'string' ? reference.projectTitle.slice(0, 160) : null, conversationTitle: typeof reference.conversationTitle === 'string' ? reference.conversationTitle.slice(0, 160) : null, projectSlug: typeof reference.projectSlug === 'string' ? reference.projectSlug.slice(0, 160) : null, sessionId: Number.isSafeInteger(reference.sessionId) ? reference.sessionId : null, screen: String(reference.screen || '').slice(0, 40), workspace: reference.workspace || null };
153
153
  } catch (error) { return null; }
154
154
  }
@@ -1,3 +1,4 @@
1
+ import { contextualViewMetadata } from './contextual-view-metadata.js';
1
2
  import { store } from './store.js';
2
3
  import { getWs } from './ws-ref.js';
3
4
  import { contextualFrame } from './contextual-mate-identity.js';
@@ -7,7 +8,7 @@ function identity(frame) {
7
8
  }
8
9
  function contextKey(frame, canonical) {
9
10
  var current = identity(frame);
10
- return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('sessionOriginId'), store.get('activeProjectSlug'), store.get('sessionActivatedProjectSlug'), store.get('replayingHistory'), (store.get('contextualClay') || {}).mode, store.get('homeShellVisible'), store.get('projectsHubVisible'), store.get('dmMode'), store.get('rightWorkbenchOwner'), store.get('homeSubSurface'), store.get('rightWorkbenchRevision'), store.get('myUserId'), store.get('connected'), current && current.mateId, current && current.sessionId, current && (canonical ? current.connected : current.ready), canonical && current && current.sessionOriginId]);
11
+ return JSON.stringify([store.get('currentSlug'), store.get('activeSessionId'), store.get('sessionOriginId'), store.get('activeProjectSlug'), store.get('sessionActivatedProjectSlug'), store.get('replayingHistory'), (store.get('contextualClay') || {}).mode, store.get('homeShellVisible'), store.get('projectsHubVisible'), store.get('dmMode'), store.get('rightWorkbenchOwner'), store.get('homeSubSurface'), store.get('rightWorkbenchRevision'), store.get('projectSplitViewActiveId'), JSON.stringify(contextualViewMetadata()), store.get('myUserId'), store.get('connected'), current && current.mateId, current && current.sessionId, current && (canonical ? current.connected : current.ready), canonical && current && current.sessionOriginId]);
11
12
  }
12
13
  export function updateMateContext(surface, mate, frame, snapshot) {
13
14
  var current = identity(frame), ws = getWs(), key = contextKey(frame, mate && mate.builtinKey === 'clay'), prepared = store.get('contextualPrepared');
@@ -18,7 +19,7 @@ export function updateMateContext(surface, mate, frame, snapshot) {
18
19
  capabilities.textContent = "I can control Clay Studio for you—manage projects, coordinate work, set up schedules, and navigate your workspace. Just tell me what you’d like done.";
19
20
  var privateWorkspace = (store.get('currentSlug') || '').indexOf('mate-') === 0;
20
21
  var parentReady = store.get('activeProjectSlug') === store.get('currentSlug') && store.get('sessionActivatedProjectSlug') === store.get('currentSlug') && !store.get('replayingHistory');
21
- var available = (store.get('contextualClay') || {}).mode === 'popover' && parentReady && !canonical && !privateWorkspace && current && current.ready && store.get('connected') && !store.get('homeShellVisible') && !store.get('projectsHubVisible') && !store.get('dmMode') && !store.get('rightWorkbenchOwner') && ws && ws.readyState === 1;
22
+ var available = (store.get('contextualClay') || {}).mode === 'popover' && parentReady && !canonical && !privateWorkspace && current && current.ready && store.get('connected') && !store.get('homeShellVisible') && !store.get('projectsHubVisible') && !store.get('dmMode') && ws && ws.readyState === 1;
22
23
  var changed = !prepared || prepared.key !== key || prepared.socket !== ws;
23
24
  if (changed) {
24
25
  var requestId = 'context-' + crypto.randomUUID();
@@ -26,7 +27,7 @@ export function updateMateContext(surface, mate, frame, snapshot) {
26
27
  }
27
28
  // Stamp the reference before publishing a new preparation: store subscribers
28
29
  // may render synchronously, and the composer must never see the prior view ID.
29
- snapshot = Object.assign({}, snapshot, { viewId: prepared.requestId });
30
+ snapshot = Object.assign({}, snapshot, { viewId: prepared.requestId, workspace: contextualViewMetadata() });
30
31
  surface.dataset.screenReference = JSON.stringify(snapshot);
31
32
  if (changed) {
32
33
  store.set({ contextualPrepared: prepared });
@@ -37,6 +38,7 @@ export function updateMateContext(surface, mate, frame, snapshot) {
37
38
  var label = surface.querySelector('[data-clay-context]');
38
39
  var detail = surface.querySelector('.contextual-clay-context p');
39
40
  var summary = ready ? prepared.summary.project + ' / ' + prepared.summary.conversation : null;
41
+ var viewName = snapshot.workspace && snapshot.workspace.name;
40
42
  var projectName = (ready && prepared.summary.project) || snapshot.projectTitle || snapshot.screen || 'Current view';
41
43
  if (canonical) {
42
44
  var currentSummary = (snapshot.projectTitle || snapshot.screen || 'Current view') + (snapshot.conversationTitle ? ' / ' + snapshot.conversationTitle : '');
@@ -51,6 +53,8 @@ export function updateMateContext(surface, mate, frame, snapshot) {
51
53
  detail.textContent = ready ? (shared && shared.viewId === prepared.requestId ? 'Shared with your last message: ' : 'With your next message: ') + summary + '. This Mate can read up to 20 text turns from this selected conversation and request to open it in this window after your review. Tool results, attachments and hidden entries are excluded.' : 'Current view: ' + projectName + '. This view has no supported conversation context. Open an accessible project conversation to share it with your next message.';
52
54
  detail.textContent += '\nNo continuous screen capture. Issues, documents, other tabs, files and settings are unavailable through this context.';
53
55
  }
56
+ if (viewName) detail.textContent += '\nView: ' + viewName + '.';
57
+ detail.textContent += '\nEach message also includes current View, session titles and open workbench names as display metadata. Workbench contents are not included.';
54
58
  if (prepared.status === 'expired' && !canonical) { contextStatus = 'Expired'; detail.textContent = 'This five-minute context has expired. Close and reopen this Mate to prepare a fresh snapshot. No previous navigation request will replay.'; }
55
59
  if (shared) detail.textContent += '\nLast submitted: ' + shared.summary.project + ' / ' + shared.summary.conversation + ((canonical ? sameSnapshot : shared.viewId === prepared.requestId) ? '.' : canonical ? '. The view or connection has changed; that snapshot is stale.' : '. Your view has changed; that context is stale.');
56
60
  var projectLabel = label.querySelector('[data-context-project]');
@@ -0,0 +1,23 @@
1
+ import { store } from './store.js';
2
+ import { getCachedSessions } from './sidebar-sessions.js';
3
+
4
+ function label(value) { return typeof value === 'string' ? value.replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, 160) : null; }
5
+
6
+ // Display labels only. Conversation access remains bound to the authenticated
7
+ // selected session; this snapshot never grants access to another pane or tool.
8
+ export function contextualViewMetadata() {
9
+ var state = store.snap();
10
+ if (state.paneMode || state.homeShellVisible || state.projectsHubVisible || state.dmMode ||
11
+ state.activeProjectSlug !== state.currentSlug || state.sessionActivatedProjectSlug !== state.currentSlug || state.replayingHistory) return null;
12
+ var view = (state.projectSplitViews || []).find(function (item) { return item.id === state.projectSplitViewActiveId; });
13
+ var sessions = getCachedSessions() || [];
14
+ var refs = view ? view.sessions || [] : sessions.filter(function (item) { return item.id === state.activeSessionId; });
15
+ var owners = (state.rightWorkbenchDockedOwners || []).concat(state.rightWorkbenchOwner || []).filter(function (name, index, all) { return all.indexOf(name) === index; });
16
+ return { name: label(view && view.name),
17
+ sessions: refs.slice(0, 32).map(function (ref) {
18
+ var session = sessions.find(function (item) { return item.id === ref.id && (!ref.sessionOriginId || item.sessionOriginId === ref.sessionOriginId); });
19
+ return session ? { sessionId: session.id, title: label(session.title), selected: session.id === state.activeSessionId,
20
+ focused: !!state.projectSplitViewFocusedOrigin && session.sessionOriginId === state.projectSplitViewFocusedOrigin } : null;
21
+ }).filter(Boolean),
22
+ workbenches: owners.slice(0, 16).map(label) };
23
+ }
@@ -70,7 +70,7 @@ function chronologicalComments(comments) {
70
70
  function commentMarkup(comment) {
71
71
  var date = new Date(comment.createdAt);
72
72
  var timestamp = Number.isFinite(date.getTime()) ? '<time datetime="' + esc(date.toISOString()) + '" title="' + esc(date.toLocaleString()) + '">' + esc(date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })) + '</time>' : '';
73
- return '<article class="linear-comment"><header><strong>' + esc(comment.author) + '</strong>' + timestamp + '</header><div class="issue-doc-body">' + prose(comment.body) + '</div></article>';
73
+ return '<article class="issue-comment linear-comment"><header class="issue-comment-meta"><strong>' + esc(comment.author) + '</strong>' + timestamp + '</header><div class="issue-doc-body">' + prose(comment.body) + '</div></article>';
74
74
  }
75
75
  function renderIssue() {
76
76
  var issue = store.get('linearIssue'), sessions = store.get('linearMatches') || [];
@@ -80,10 +80,10 @@ function renderIssue() {
80
80
  var linked = !!(active && (active.linearLinks || []).some(function (item) { return item.url === issue.url; }));
81
81
  var prs = active && linked ? (active.githubLinks || []).filter(function (item) { return item.kind === 'pr' && /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/[1-9][0-9]*$/.test(item.url); }) : [];
82
82
  content().innerHTML = '<article class="issue-detail"><div class="linear-issue-meta"><span class="linear-issue-id" aria-label="Issue identifier">' + esc(issue.identifier) + '</span>' + (issue.stateName ? '<span class="linear-issue-status" aria-label="Status">' + esc(issue.stateName) + '</span>' : '') + '</div><h2 class="issue-doc-title">' + esc(issue.title) + '</h2>' +
83
- '<section class="linear-conversations"><h3>Linked conversations</h3><div class="linear-session-pills">' +
84
- sessions.map(function (item) { return '<button type="button" class="linear-session-pill" data-linear-session="' + esc(item.sessionId) + '"><i data-lucide="message-square"></i><span>' + esc(item.title || 'Open conversation') + '</span></button>'; }).join('') +
85
- '<button type="button" class="linear-session-pill linear-action-chip" data-linear-link' + (!store.get('activeSessionId') ? ' disabled' : '') + '><i data-lucide="' + (linked ? 'unlink' : 'plus') + '"></i><span>' + (linked ? 'Unlink this conversation' : 'Link this conversation') + '</span></button>' +
86
- (!sessions.length ? '<button type="button" class="linear-session-pill linear-action-chip" data-linear-start><i data-lucide="plus"></i><span>Start work</span></button>' : '') +
83
+ '<section class="linear-conversations"><h3>Linked conversations</h3><div class="workbench-related-links linear-session-pills">' +
84
+ sessions.map(function (item) { return '<button type="button" class="workbench-related-link linear-session-pill" data-linear-session="' + esc(item.sessionId) + '"><i data-lucide="message-square"></i><span>' + esc(item.title || 'Open conversation') + '</span></button>'; }).join('') +
85
+ '<button type="button" class="workbench-related-link linear-session-pill linear-action-chip" data-linear-link' + (!store.get('activeSessionId') ? ' disabled' : '') + '><i data-lucide="' + (linked ? 'unlink' : 'plus') + '"></i><span>' + (linked ? 'Unlink this conversation' : 'Link this conversation') + '</span></button>' +
86
+ (!sessions.length ? '<button type="button" class="workbench-related-link linear-session-pill linear-action-chip" data-linear-start><i data-lucide="plus"></i><span>Start work</span></button>' : '') +
87
87
  '</div><p class="linear-sharing-hint">Linked issue titles and status are visible to conversation viewers.</p></section>' +
88
88
  (prs.length ? '<section><h3>GitHub pull requests</h3>' + prs.map(function (pr) { return '<a target="_blank" rel="noopener noreferrer" href="' + esc(pr.url) + '">PR #' + esc(pr.number) + ' · ' + esc(pr.title) + '</a>'; }).join('<br>') + '</section>' : '') +
89
89
  '<div class="issue-doc-body">' + prose(issue.description) + '</div><section class="issue-discussion"><h3>Comments</h3>' +
@@ -111,7 +111,7 @@ export function filterPaneActivation(message) {
111
111
  var pending = store.get('panePendingBinding'), committed = store.get('paneCommittedBinding');
112
112
  if (message.type === 'pane_activation_rejected') { failed(message.requestId); return true; }
113
113
  if (!pending && !committed) return !!message.paneActivation;
114
- if (['session_list', 'split_groups', 'session_presence', 'pong', 'account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0 && !message.paneActivation) return false;
114
+ if (['session_list', 'split_groups', 'session_presence', 'mate_list', 'mate_updated', 'mate_deleted', 'pong', 'account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0 && !message.paneActivation) return false;
115
115
  var expected = pending || committed;
116
116
  if (!samePaneBinding(expected, message.paneActivation)) return true;
117
117
  if (message.type === 'pane_activation_started') return true;
@@ -139,6 +139,10 @@ export function filterPaneActivation(message) {
139
139
  clearDeadline();
140
140
  store.set({ paneCommittedBinding: expected, panePendingBinding: null, paneReady: true });
141
141
  block(false);
142
+ // Initial owner metadata may have arrived during activation. Read it again
143
+ // after committing; session-bound actions still require their live gesture.
144
+ var socket = getWs();
145
+ if (socket && socket.readyState === 1) socket.send(JSON.stringify({ type: 'mate_list' }));
142
146
  if (typeof performance !== 'undefined' && performance.mark) { performance.clearMarks('clay:pane-ready'); performance.mark('clay:pane-ready'); }
143
147
  var frame = window.frameElement;
144
148
  if (frame) frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-ready'));
@@ -5,7 +5,7 @@ export function renderBrowserTabs(el, select, send) {
5
5
  var bar = el.querySelector('.shared-browser-tabs');
6
6
  var tabs = store.get('sharedBrowsers') || [];
7
7
  var selected = store.get('sharedBrowser');
8
- var key = JSON.stringify([tabs.map(function (tab) { return [tab.id, tab.url, tab.control, tab.phase, tab.incognito, tab.personalCookiesAvailable]; }), selected && selected.id, store.get('connected')]);
8
+ var key = JSON.stringify([tabs.map(function (tab) { return [tab.id, tab.url, tab.control, tab.phase, tab.incognito, tab.personalCookiesAvailable, tab.sourceSessionId, tab.sourceRole, tab.sourceTitle, tab.sourceGeneration]; }), selected && selected.id, store.get('connected')]);
9
9
  if (bar.dataset.key === key) return;
10
10
  var focused = bar.contains(document.activeElement) ? document.activeElement : null;
11
11
  var focusedId = focused && focused.dataset.browserId, focusedAction = focused && focused.dataset.tabAction;
@@ -23,8 +23,10 @@ export function renderBrowserTabs(el, select, send) {
23
23
  button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1;
24
24
  var title = 'New tab';
25
25
  try { if (tab.url) title = new URL(tab.url).hostname || 'New tab'; } catch (e) {}
26
+ var source = tab.sourceRole === 'worker' ? 'Worker #' + tab.sourceSessionId : '';
27
+ if (source) title = source + ' · ' + title;
26
28
  var mode = tab.phase === 'ended' ? 'Ended' : tab.phase === 'starting' ? 'Starting' : tab.control === 'agent' ? 'You and Clay can control this tab' : 'Only you can control this tab. Clay is paused.';
27
- var description = title + ' · ' + (tab.incognito === false ? 'Personal browser' : 'Incognito') + ' · ' + mode;
29
+ var description = title + (source ? ' · ' + tab.sourceTitle + ' · Gen ' + tab.sourceGeneration : '') + ' · ' + (tab.incognito === false ? 'Personal browser' : 'Incognito') + ' · ' + mode;
28
30
  var label = document.createElement('span'); label.className = 'shared-browser-tab-label'; label.textContent = title;
29
31
  button.innerHTML = iconHtml(tab.personalCookiesAvailable && tab.incognito ? 'glasses' : 'globe');
30
32
  button.appendChild(label); button.setAttribute('aria-label', description); button.setAttribute('data-tip', description);