clay-server 5.2.0-beta.6 → 5.2.0-beta.8

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 (109) hide show
  1. package/lib/clay-control-presentation-binding.js +96 -0
  2. package/lib/clay-control-presentation.js +128 -0
  3. package/lib/clay-control-target-projection.js +26 -0
  4. package/lib/clay-mate-chat.js +1 -0
  5. package/lib/clay-mate-knowledge.js +1 -1
  6. package/lib/clay-product-control-tools.js +1 -1
  7. package/lib/clay-product-control.js +22 -12
  8. package/lib/clay-project-records.js +2 -1
  9. package/lib/clay-scheduling.js +1 -1
  10. package/lib/clay-screen-controls.js +13 -5
  11. package/lib/clay-workspace-controls.js +2 -1
  12. package/lib/contextual-screen-snapshot.js +38 -0
  13. package/lib/mate-context-service.js +138 -0
  14. package/lib/mate-context-tools.js +21 -0
  15. package/lib/project-connection.js +8 -5
  16. package/lib/project-control-git-view.js +12 -0
  17. package/lib/project-filesystem.js +5 -1
  18. package/lib/project-http.js +3 -0
  19. package/lib/project-notifications.js +5 -4
  20. package/lib/project-product-control.js +3 -1
  21. package/lib/project-scheduled-messages.js +4 -4
  22. package/lib/project-scheduled-task-control.js +1 -1
  23. package/lib/project-session-document.js +20 -5
  24. package/lib/project-shared-browser.js +22 -5
  25. package/lib/project-studio-presentation.js +58 -0
  26. package/lib/project-user-message.js +36 -0
  27. package/lib/project-workspace-query.js +11 -1
  28. package/lib/project.js +24 -7
  29. package/lib/public/app.js +24 -0
  30. package/lib/public/appearance-bootstrap.js +2 -0
  31. package/lib/public/css/contextual-clay.css +240 -0
  32. package/lib/public/modules/app-favicon.js +7 -7
  33. package/lib/public/modules/app-message-router.js +5 -1
  34. package/lib/public/modules/app-messages.js +14 -1
  35. package/lib/public/modules/app-notifications.js +10 -3
  36. package/lib/public/modules/app-projects.js +3 -0
  37. package/lib/public/modules/clay-control-activity.js +214 -0
  38. package/lib/public/modules/clay-control-copy.js +36 -0
  39. package/lib/public/modules/clay-control-detail-targets.js +44 -0
  40. package/lib/public/modules/clay-control-object-targets.js +52 -0
  41. package/lib/public/modules/clay-control-target-visibility.js +12 -0
  42. package/lib/public/modules/clay-control-targets.js +44 -0
  43. package/lib/public/modules/clay-navigation-render.js +52 -0
  44. package/lib/public/modules/clay-owner-navigation.js +11 -2
  45. package/lib/public/modules/clay-session-links.js +31 -0
  46. package/lib/public/modules/clay-studio-bridge.js +20 -0
  47. package/lib/public/modules/clay-studio-document.js +67 -0
  48. package/lib/public/modules/clay-studio-render.js +144 -0
  49. package/lib/public/modules/clay-tool-presentation.js +4 -0
  50. package/lib/public/modules/command-palette.js +26 -31
  51. package/lib/public/modules/contextual-ask-session.js +26 -0
  52. package/lib/public/modules/contextual-clay-geometry.js +155 -0
  53. package/lib/public/modules/contextual-clay-session.js +130 -0
  54. package/lib/public/modules/contextual-clay-workspace.js +157 -0
  55. package/lib/public/modules/contextual-clay.js +237 -0
  56. package/lib/public/modules/contextual-composer.js +91 -0
  57. package/lib/public/modules/contextual-mate-context.js +115 -0
  58. package/lib/public/modules/contextual-mate-hint.js +40 -0
  59. package/lib/public/modules/contextual-mate-identity.js +23 -0
  60. package/lib/public/modules/contextual-mate-sessions.js +134 -0
  61. package/lib/public/modules/contextual-search.js +137 -0
  62. package/lib/public/modules/contextual-session-citations.js +58 -0
  63. package/lib/public/modules/filebrowser.js +20 -1
  64. package/lib/public/modules/generation-stop.js +8 -5
  65. package/lib/public/modules/git-panel.js +3 -1
  66. package/lib/public/modules/icon-strip-tooltip.js +2 -1
  67. package/lib/public/modules/input.js +35 -1
  68. package/lib/public/modules/issues-render.js +3 -2
  69. package/lib/public/modules/knowledge-db-workbench.js +4 -4
  70. package/lib/public/modules/markdown.js +2 -34
  71. package/lib/public/modules/mate-knowledge-workbench.js +1 -1
  72. package/lib/public/modules/message-delivery.js +10 -0
  73. package/lib/public/modules/pane-bridge.js +2 -2
  74. package/lib/public/modules/pending-message-queue.js +1 -0
  75. package/lib/public/modules/product-review.js +26 -0
  76. package/lib/public/modules/project-chat-identity.js +13 -4
  77. package/lib/public/modules/project-logs-render.js +3 -0
  78. package/lib/public/modules/project-mate-navigation.js +58 -21
  79. package/lib/public/modules/scheduled-tasks.js +2 -0
  80. package/lib/public/modules/search-clay-chat.js +5 -454
  81. package/lib/public/modules/shared-browser.js +2 -0
  82. package/lib/public/modules/sidebar-mobile.js +4 -3
  83. package/lib/public/modules/sidebar-projects.js +1 -0
  84. package/lib/public/modules/sidebar-sessions.js +1 -0
  85. package/lib/public/modules/split-view.js +7 -16
  86. package/lib/public/modules/stt.js +7 -2
  87. package/lib/public/modules/terminal.js +9 -0
  88. package/lib/public/modules/tools.js +4 -1
  89. package/lib/public/modules/wireframe-workbench.js +5 -2
  90. package/lib/public/style.css +2 -0
  91. package/lib/sdk-bridge.js +4 -0
  92. package/lib/server-contextual-clay-settings.js +40 -0
  93. package/lib/server-contextual-mate-sessions.js +70 -0
  94. package/lib/server-home-chat.js +7 -1
  95. package/lib/server-home-clay-entry.js +23 -4
  96. package/lib/server-home-clay-session-links.js +12 -0
  97. package/lib/server-owner-ui.js +49 -8
  98. package/lib/server-settings.js +2 -0
  99. package/lib/server.js +17 -0
  100. package/lib/sessions.js +10 -3
  101. package/lib/terminal-manager.js +18 -11
  102. package/lib/users-contextual-clay-preferences.js +31 -0
  103. package/lib/users.js +3 -0
  104. package/lib/workspace-assignment-service.js +25 -0
  105. package/lib/workspace-query-mcp-server.js +1 -1
  106. package/lib/ws-request.js +1 -0
  107. package/lib/ws-schema.js +10 -0
  108. package/lib/yoke/claude-hook-installer.js +4 -0
  109. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { forwardStudioPresentation, registerStudioPaint } from './clay-studio-render.js';
1
2
  import { sketchBrand, sketchCredit } from './wireframe-style.js';
2
3
  import { store } from './store.js';
3
4
  import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
@@ -80,7 +81,7 @@ function createPanel() {
80
81
  export function openWireframe(message) {
81
82
  if (!message || typeof message.source !== 'string' || new TextEncoder().encode(message.source).length > 100000) return;
82
83
  if (store.get('paneMode') && window.parent !== window) {
83
- window.parent.postMessage({ type: 'clay-pane-present-wireframe', message: message, projectSlug: store.get('currentSlug'), sessionId: store.get('activeSessionId') }, window.location.origin);
84
+ forwardStudioPresentation(message, 'clay-pane-present-wireframe');
84
85
  return;
85
86
  }
86
87
  claimRightWorkbench('wireframe');
@@ -92,7 +93,8 @@ export function openWireframe(message) {
92
93
  el.querySelector('header strong').textContent = draft.title;
93
94
  el.querySelector('.wireframe-save-status').textContent = '';
94
95
  el.querySelector('form').hidden = true;
95
- draw(draft, el).catch(function () {});
96
+ var painted = draw(draft, el); registerStudioPaint(Object.assign({}, message, { source: draft.source }), el.querySelector("header"), painted, el.querySelector(".wireframe-workbench-canvas"));
97
+ painted.catch(function () {});
96
98
  }
97
99
  function draw(draft, el) {
98
100
  var canvas = el.querySelector('.wireframe-workbench-canvas');
@@ -104,6 +106,7 @@ function draw(draft, el) {
104
106
  var image = wireframeImage(svg, draft.title);
105
107
  canvas.replaceChildren(image);
106
108
  mountWireframeViewport(canvas, image, el.querySelector('.wireframe-zoom'));
109
+ return image.decode ? image.decode() : undefined;
107
110
  }).catch(function (error) {
108
111
  if (store.get('wireframeDraft') !== active) throw error;
109
112
  status.textContent = error.name === 'AbortError' ? 'Rendering was interrupted. Reopen the wireframe to retry.' : error.message;
@@ -90,3 +90,5 @@
90
90
  @import url("css/project-settings-modal.css");
91
91
 
92
92
  @import url("css/clay-tool-activity.css");
93
+
94
+ @import url("css/contextual-clay.css");
package/lib/sdk-bridge.js CHANGED
@@ -711,6 +711,10 @@ function createSDKBridge(opts) {
711
711
  // Workspace inspection and proposal posting are session-bound. Keep this
712
712
  // list exact so a future execution/mutation tool cannot inherit approval.
713
713
  var safeWorkspaceTools = {
714
+ "mcp__clay-workspace__read_current_context": true,
715
+ "mcp__clay-workspace__read_selected_context": true,
716
+ "mcp__clay-workspace__open_context_conversation": true,
717
+ "mcp__clay-workspace__read_context_navigation_receipt": true,
714
718
  "mcp__clay-workspace__read_owner_screen": true,
715
719
  "mcp__clay-workspace__inspect_owner_screen_change": true,
716
720
  "mcp__clay-workspace__control_owner_screen": true,
@@ -0,0 +1,40 @@
1
+ // Authenticated personal preferences, using the shared users/config persistence.
2
+ function attachContextualClaySettings(ctx) {
3
+ function reply(res, status, data) {
4
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
5
+ res.end(JSON.stringify(data));
6
+ }
7
+ function identity(req) {
8
+ if (!ctx.isRequestAuthed(req)) return null;
9
+ return ctx.users.isMultiUser() ? ctx.getMultiUserFromReq(req) : { id: 'default' };
10
+ }
11
+ function handleRequest(req, res, url) {
12
+ if (url !== '/api/user/contextual-clay-geometry') return false;
13
+ var user = identity(req);
14
+ if (!user) { reply(res, 401, { error: 'Sign in to manage popover preferences.' }); return true; }
15
+ if (req.method === 'GET') {
16
+ try { reply(res, 200, { geometry: ctx.users.getContextualClayGeometry(user.id) }); }
17
+ catch (error) { reply(res, 500, { error: 'Could not read popover preference.' }); }
18
+ return true;
19
+ }
20
+ if (req.method !== 'PUT') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
21
+ if (req.headers['x-clay-settings'] !== '1' || (req.headers['sec-fetch-site'] && ['same-origin', 'none'].indexOf(req.headers['sec-fetch-site']) < 0)) {
22
+ reply(res, 403, { error: 'Save preferences from Clay.' }); return true;
23
+ }
24
+ var body = ''; var tooLarge = false;
25
+ req.setEncoding('utf8');
26
+ req.on('data', function (chunk) { body += chunk; if (body.length > 1024 && !tooLarge) { tooLarge = true; reply(res, 413, { error: 'Request too large.' }); } });
27
+ req.on('end', function () {
28
+ if (tooLarge) return;
29
+ var live = identity(req);
30
+ if (!live || live.id !== user.id) { reply(res, 401, { error: 'Sign in again.' }); return; }
31
+ try {
32
+ var result = ctx.users.setContextualClayGeometry(live.id, JSON.parse(body));
33
+ reply(res, result.error ? 400 : 200, result);
34
+ } catch (error) { reply(res, 400, { error: 'Could not save popover preference.' }); }
35
+ });
36
+ return true;
37
+ }
38
+ return { handleRequest: handleRequest };
39
+ }
40
+ module.exports = { attachContextualClaySettings: attachContextualClaySettings };
@@ -0,0 +1,70 @@
1
+ // Presentation-only session operations share Home's authoritative permissions,
2
+ // model policy, saved-session projection and session manager. No transcript tap.
3
+ function attachContextualMateSessions(ctx) {
4
+ var receipts = new Map();
5
+ var lifetime = 30 * 60 * 1000;
6
+ function stamp(mate) { return JSON.stringify([mate.id, mate.createdBy || null, mate.createdAt || null, mate.builtinKey || null]); }
7
+ function live(ws, userId, msg, original) {
8
+ if (!ctx.isActorLive(ws, userId)) throw new Error('Your account is no longer available.');
9
+ var found = ctx.findMateProject(userId, msg.mateId, true);
10
+ if (!found || found.mate.archived || found.mate.lifecycleDeletion || found.mate.createdBy && found.mate.createdBy !== userId || stamp(found.mate) !== msg.mateStamp) throw new Error('This Mate is no longer available.');
11
+ var status = found.ctx.getStatus();
12
+ if (!status || !status.isMate || status.mateId !== found.mate.id || userId && status.projectOwnerId !== userId) throw new Error('This private Mate workspace is unavailable.');
13
+ found.contextualManager = found.ctx.getSessionManager();
14
+ found.contextualRevision = JSON.stringify([status.path, status.projectOwnerId, status.mateId, stamp(found.mate)]);
15
+ if (original && (found.ctx !== original.ctx || found.contextualRevision !== original.contextualRevision || found.contextualManager !== original.contextualManager)) throw new Error('This Mate conversation changed.');
16
+ return found;
17
+ }
18
+ function reply(ws, msg, data) { ctx.sendMessage(ws, Object.assign({ type: 'contextual_mate_sessions_result', requestId: msg.requestId, mateId: msg.mateId, mateStamp: msg.mateStamp }, data)); }
19
+ async function perform(ws, userId, msg) {
20
+ try {
21
+ if (typeof msg.requestId !== 'string' || !/^mate-conversations-[a-z0-9-]{36}$/.test(msg.requestId)) throw new Error('Invalid conversation request.');
22
+ var found = live(ws, userId, msg), manager = found.ctx.getSessionManager();
23
+ if (!manager) throw new Error('Conversation history is unavailable.');
24
+ if (msg.type === 'home_mate_sessions_list') {
25
+ var list = ctx.listHomeSessions(found, userId);
26
+ reply(ws, msg, { ok: true, sessions: list.slice(0, 500), truncated: list.length > 500 }); return;
27
+ }
28
+ var session;
29
+ if (msg.type === 'home_mate_session_open') {
30
+ session = ctx.resolveHomeSession(found, userId, msg.sessionId);
31
+ if (!session || typeof msg.sessionOriginId !== 'string' || msg.sessionOriginId !== session.sessionOriginId) throw new Error('This conversation is no longer available.');
32
+ } else {
33
+ var now = Date.now();
34
+ receipts.forEach(function (entry, key) { if (now - entry.at > lifetime) receipts.delete(key); });
35
+ var key = JSON.stringify([userId, msg.mateId, msg.requestId]);
36
+ var receipt = receipts.get(key);
37
+ manager.sessions.forEach(function (candidate) { if (candidate.contextualCreationRequestId === msg.requestId && ctx.ownsSession(candidate, userId)) session = candidate; });
38
+ if (session && session.contextualCreationMateStamp !== msg.mateStamp) throw new Error('This request belongs to a replaced Mate.');
39
+ if (receipt && (!session || receipt.origin !== session.sessionOriginId || receipt.stamp !== msg.mateStamp)) throw new Error('The previously created conversation is no longer available.');
40
+ if (!session && msg.lookupOnly) throw new Error('Creation was not confirmed. Start a new chat explicitly if you still want one.');
41
+ if (!session) {
42
+ if (receipts.size >= 1024) throw new Error('Please try starting a conversation later.');
43
+ var resolved = await ctx.homeModels.resolveMateModel(ws, found, userId);
44
+ found = live(ws, userId, msg, found);
45
+ // Recheck after model resolution: concurrent retries select one exact session.
46
+ manager.sessions.forEach(function (candidate) { if (candidate.contextualCreationRequestId === msg.requestId && ctx.ownsSession(candidate, userId)) session = candidate; });
47
+ receipt = receipts.get(key);
48
+ if (receipt && (!session || receipt.origin !== session.sessionOriginId || receipt.stamp !== msg.mateStamp)) throw new Error('The previously created conversation is no longer available.');
49
+ if (session && session.contextualCreationMateStamp !== msg.mateStamp) throw new Error('This request belongs to a replaced Mate.');
50
+ if (!session) {
51
+ if (receipts.size >= 1024) throw new Error('Please try starting a conversation later.');
52
+ session = manager.createSessionRaw({ ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null });
53
+ session.contextualCreationRequestId = msg.requestId;
54
+ session.contextualCreationMateStamp = msg.mateStamp;
55
+ manager.saveSessionFile(session);
56
+ }
57
+ }
58
+ if (!ctx.ownsSession(session, userId)) throw new Error('This conversation is no longer available.');
59
+ receipts.set(key, { at: now, origin: session.sessionOriginId, stamp: msg.mateStamp });
60
+ }
61
+ live(ws, userId, msg, found);
62
+ reply(ws, msg, { ok: true, sessionId: session.localId, sessionOriginId: session.sessionOriginId });
63
+ } catch (error) { reply(ws, msg, { ok: false, error: error.message || 'Conversation unavailable.' }); }
64
+ }
65
+ return { handle: function (ws, userId, msg) {
66
+ if (msg.presentation !== 'contextual' || !/^home_mate_(sessions_list|session_open|new_session)$/.test(msg.type)) return false;
67
+ perform(ws, userId, msg); return true;
68
+ } };
69
+ }
70
+ module.exports = { attachContextualMateSessions: attachContextualMateSessions };
@@ -123,6 +123,7 @@ function attachHomeChat(deps) {
123
123
  model: sessionString(session.model, 240),
124
124
  createdAt: createdAt,
125
125
  lastActivity: lastActivity,
126
+ sessionOriginId: session.sessionOriginId || null,
126
127
  isProcessing: !!session.isProcessing,
127
128
  sessionRole: hierarchy[session.localId].role,
128
129
  parentSessionId: hierarchy[session.localId].parentSessionId == null ? null : sessionReference(sessionManager.sessions.get(hierarchy[session.localId].parentSessionId)),
@@ -228,6 +229,7 @@ function attachHomeChat(deps) {
228
229
  var homeDebates = attachHomeDebates({ mates: mates, findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, sendMessage: sendMessage });
229
230
  var homeClaySessionLinks = attachHomeClaySessionLinks({
230
231
  projects: projects,
232
+ findMateProject: findMateProject, ownsSession: ownsSession, isActorLive: isActorLive,
231
233
  workspaceQueryService: deps.workspaceQueryService,
232
234
  projectLogsService: deps.projectLogsService,
233
235
  sendMessage: sendMessage,
@@ -300,7 +302,9 @@ function attachHomeChat(deps) {
300
302
  });
301
303
  var homeMateCreation = attachHomeMateCreation({ findMateProject: findMateProject, resolveHomeSession: resolveHomeSession, sessionReference: sessionReference, setupTap: setupTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, homeModels: homeModels, isActorLive: isActorLive, persistActiveSession: persistActiveSession });
302
304
  var homeMcpConsent = require('./server-home-mcp-consent').attachHomeMcpConsent({ findMateProject: findMateProject, resolveHomeSession: resolveHomeSession, messageTap: messageTap, isActorLive: isActorLive, sendError: sendError });
303
- var homeClayEntry = attachHomeClayEntry({ findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, setupSearchTap: setupSearchTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, homeModels: homeModels, isActorLive: isActorLive });
305
+ var homeClayEntry = attachHomeClayEntry({ findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, setupSearchTap: setupSearchTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, sendMessage: sendMessage, homeModels: homeModels, isActorLive: isActorLive });
306
+
307
+ var contextualSessions = require('./server-contextual-mate-sessions').attachContextualMateSessions({ findMateProject: findMateProject, ownsSession: ownsSession, resolveHomeSession: resolveHomeSession, listHomeSessions: listHomeSessions, isActorLive: isActorLive, homeModels: homeModels, sendMessage: sendMessage });
304
308
 
305
309
  async function openExactSession(ws, found, userId, selected, msg) {
306
310
  await ensureSessionModel(ws, found, userId, selected, null);
@@ -448,6 +452,8 @@ function attachHomeChat(deps) {
448
452
  return false;
449
453
  }
450
454
 
455
+ if (contextualSessions.handle(ws, ws._clayUser ? ws._clayUser.id : null, msg)) return true;
456
+
451
457
  if (msg.type === "home_mate_present") {
452
458
  ws._homeChatPresented = msg.visible === true;
453
459
  return true;
@@ -15,10 +15,12 @@ function attachHomeClayEntry(ctx) {
15
15
  }
16
16
 
17
17
  async function start(ws, userId, msg) {
18
+ var contextual = msg.presentation === 'contextual';
18
19
  var text = typeof msg.text === "string" ? msg.text.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 12000) : "";
19
20
  var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 240) : null;
21
+ if (ctx.isActorLive && !ctx.isActorLive(ws, userId)) { ctx.sendError(ws, null, 'Your account is no longer available.', requestId, null, 'actor_unavailable'); return; }
20
22
  var found = ctx.findMateProject(userId, null, true);
21
- if (!found || found.mate.builtinKey !== "clay") {
23
+ if (!found || found.mate.builtinKey !== "clay" || found.mate.archived || ctx.isActorLive && !ctx.isActorLive(ws, userId)) {
22
24
  ctx.sendError(ws, null, "Clay is not available.", requestId, null, "mate_unavailable");
23
25
  return;
24
26
  }
@@ -27,26 +29,43 @@ function attachHomeClayEntry(ctx) {
27
29
  return;
28
30
  }
29
31
  var manager = found.ctx.getSessionManager();
32
+ var mateStamp = JSON.stringify([found.mate.id, found.mate.createdAt || null, found.mate.createdBy || null]);
30
33
  if (!manager || typeof manager.sendAndRecord !== "function") {
31
34
  ctx.sendError(ws, found.mate.id, "Conversation history is unavailable.", requestId, null, "session_unavailable");
32
35
  return;
33
36
  }
34
37
  var existing = findExisting(manager, userId, requestId);
35
38
  if (existing) {
39
+ if (contextual) {
40
+ if (existing.homeClayEntryMode !== 'contextual' || existing.homeClayEntryText !== text || msg.sessionOriginId && msg.sessionOriginId !== existing.sessionOriginId) { ctx.sendError(ws, found.mate.id, 'This request belongs to a different conversation.', requestId, null, 'request_mismatch'); return; }
41
+ ctx.sendMessage(ws, { type: 'home_clay_contextual_session', requestId: requestId, mateId: found.mate.id, sessionId: existing.localId, sessionOriginId: existing.sessionOriginId }); return;
42
+ }
36
43
  ctx.setupSearchTap(ws, found, existing.localId, requestId);
37
44
  ctx.sendHistory(ws, found, existing, requestId);
38
45
  ctx.sendSessionList(ws, found, userId);
39
46
  return;
40
47
  }
48
+ if (contextual && msg.sessionOriginId) { ctx.sendError(ws, found.mate.id, 'This new conversation is no longer available. Start a new Ask.', requestId, null, 'session_unavailable'); return; }
41
49
  var session = null;
42
50
  try {
43
51
  var resolved = await ctx.homeModels.resolveMateModel(ws, found, userId);
44
52
  if (ctx.isActorLive && !ctx.isActorLive(ws, userId)) throw new Error("Your account is no longer available.");
45
53
  var liveFound = ctx.findMateProject(userId, null, true);
46
- if (!liveFound || liveFound.ctx !== found.ctx || !liveFound.mate || liveFound.mate.builtinKey !== "clay") throw new Error("Clay is no longer available.");
47
- session = manager.createSession({ ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null }, null);
54
+ if (!liveFound || liveFound.ctx !== found.ctx || !liveFound.mate || liveFound.ctx.getSessionManager() !== manager || liveFound.mate.builtinKey !== "clay" || JSON.stringify([liveFound.mate.id, liveFound.mate.createdAt || null, liveFound.mate.createdBy || null]) !== mateStamp || liveFound.mate.archived) throw new Error("Clay is no longer available.");
55
+ // Another request may have completed while model resolution was awaiting.
56
+ if (findExisting(manager, userId, requestId)) return start(ws, userId, msg);
57
+ var options = { ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null };
58
+ // Background creation must not switch any existing pane/native writer.
59
+ session = contextual ? manager.createSessionRaw(options) : manager.createSession(options, null);
48
60
  session.homeClayEntryRequestId = requestId;
49
- session.homeClayEntryMode = "search";
61
+ session.homeClayEntryMode = contextual ? 'contextual' : 'search';
62
+ if (contextual) {
63
+ session.homeClayEntryText = text;
64
+ session.title = text.slice(0, 80);
65
+ manager.saveSessionFile(session);
66
+ ctx.sendMessage(ws, { type: 'home_clay_contextual_session', requestId: requestId, mateId: found.mate.id, sessionId: session.localId, sessionOriginId: session.sessionOriginId });
67
+ return;
68
+ }
50
69
  manager.sendAndRecord(session, { type: "user_message", text: text });
51
70
  session.isProcessing = true;
52
71
  session.sentToolResults = {};
@@ -3,6 +3,18 @@
3
3
 
4
4
  function attachHomeClaySessionLinks(ctx) {
5
5
  function source(ws, msg, routedProjectSlug) {
6
+ if (Object.prototype.hasOwnProperty.call(msg, "contextualSource")) {
7
+ var hint = msg.contextualSource, userId = ws && ws._clayUser && ws._clayUser.id;
8
+ if (!hint || typeof hint.mateId !== "string" || !Number.isSafeInteger(hint.sessionId) || typeof hint.sessionOriginId !== "string" || typeof hint.mateStamp !== "string" || !ctx.isActorLive || !ctx.isActorLive(ws, userId)) return {};
9
+ var found = ctx.findMateProject && ctx.findMateProject(userId, hint.mateId, false);
10
+ if (!found || found.mate.archived || found.mate.lifecycleDeletion || found.mate.createdBy && found.mate.createdBy !== userId) return {};
11
+ var stamp = JSON.stringify([found.mate.id, found.mate.createdBy || null, found.mate.createdAt || null, found.mate.builtinKey || null]);
12
+ if (stamp !== hint.mateStamp || ctx.projects.get(found.slug) !== found.ctx) return {};
13
+ var manager = found.ctx.getSessionManager && found.ctx.getSessionManager();
14
+ var session = manager && manager.sessions.get(hint.sessionId);
15
+ if (!ctx.ownsSession(session, userId) || session.sessionOriginId !== hint.sessionOriginId) return {};
16
+ return { tap: { mateSlug: found.slug, mateId: found.mate.id, sessionId: hint.sessionId }, project: found.ctx, session: session };
17
+ }
6
18
  var tap = msg.surface === "search" ? ws._searchClayTap : ws._homeChatTap;
7
19
  if (!tap && routedProjectSlug) {
8
20
  var routedProject = ctx.projects.get(routedProjectSlug);
@@ -2,6 +2,27 @@ var crypto = require('crypto');
2
2
  // Only authenticated application sockets enumerated by the server are targets.
3
3
  function attachOwnerUi(ctx) {
4
4
  var identities = new WeakMap(), capabilities = new WeakMap(), deliveries = new Map();
5
+ var activityOrder = 0;
6
+ function activity(owner, slug, sessionId, mateId, selected, source, validateSource, navigationTarget) {
7
+ var order = ++activityOrder;
8
+ var viewers = selected ? [check(owner, selected)] : sockets(owner).filter(function (viewer) {
9
+ var tap = viewer.ws._homeChatTap;
10
+ return viewer.slug === slug && viewer.sessionId === sessionId || tap && tap.mateSlug === slug && tap.sessionId === sessionId;
11
+ });
12
+ function send(message, receiptViewer) {
13
+ viewers.forEach(function (viewer) {
14
+ try {
15
+ if (receiptViewer && viewer.ws !== receiptViewer || !alive(viewer.ws, owner)) return;
16
+ if (message.phase !== 'clear' && validateSource) validateSource();
17
+ var live = inspect(owner, viewer.ref), tap = live.ws._homeChatTap;
18
+ if (message.phase !== 'clear' && (selected ? (live.slug !== viewer.slug || live.sessionId !== viewer.sessionId) && !(message.phase === 'rendered' && navigationTarget && live.slug === navigationTarget.projectSlug && live.sessionId === navigationTarget.sessionId) : !(live.slug === slug && live.sessionId === sessionId || tap && tap.mateSlug === slug && tap.sessionId === sessionId))) return;
19
+ live.ws.send(JSON.stringify(Object.assign({ type: 'clay_control_activity', ownerId: owner, mateId: mateId,
20
+ sourceSlug: slug, sourceSessionId: sessionId, order: order, recipientViewerRef: live.ref }, source, message)));
21
+ } catch (error) { /* Presentation cannot alter the authoritative effect. */ }
22
+ });
23
+ }
24
+ return { send: send, clear: function (id, sequence) { send({ version: 1, id: id, sequence: sequence, phase: 'clear' }); } };
25
+ }
5
26
  function alive(ws, owner) {
6
27
  return ws && ws.readyState === 1 && (!ws._clayAuthCheck || ws._clayAuthCheck()) &&
7
28
  (ctx.isMultiUser() ? !!owner && ws._clayUser && ws._clayUser.id === owner && !!ctx.findUser(owner) : !ws._clayUser);
@@ -27,7 +48,7 @@ function attachOwnerUi(ctx) {
27
48
  if (live.ws !== original.ws || live.slug !== original.slug || live.sessionId !== original.sessionId || live.supported !== original.supported) throw new Error('The selected viewer context changed. Inspect again.');
28
49
  return live;
29
50
  }
30
- function deliver(owner, original, target, revalidate) {
51
+ function deliver(owner, original, target, revalidate, activity) {
31
52
  revalidate(); var viewer = check(owner, original);
32
53
  if (!viewer.supported) return { status: 'unsupported', delivered: false, acknowledged: false, visible: 'unknown' };
33
54
  if (deliveries.size >= 100) {
@@ -35,9 +56,15 @@ function attachOwnerUi(ctx) {
35
56
  if (deliveries.size >= 100) throw new Error('UI delivery capacity is full; wait for earlier attempts to expire.');
36
57
  }
37
58
  var id = 'navigation:' + crypto.randomUUID();
38
- var record = { owner: owner, viewer: viewer, target: target, revalidate: revalidate, created: Date.now(), claimed: false,
59
+ var renderToken = crypto.randomUUID();
60
+ var record = { owner: owner, viewer: viewer, target: target, revalidate: revalidate, activity: activity, created: Date.now(), claimed: false, renderToken: renderToken, renderClosed: false,
39
61
  result: { status: 'delivered', deliveryId: id, delivered: false, acknowledged: false, visible: 'unknown' } };
40
62
  deliveries.set(id, record);
63
+ record.renderTimer = setTimeout(function () {
64
+ if (record.renderClosed) return;
65
+ record.renderClosed = true; record.result.renderStatus = 'unknown';
66
+ if (record.activity) record.activity.navigation('unknown');
67
+ }, ctx.renderTimeoutMs || 5000); record.renderTimer.unref();
41
68
  try {
42
69
  viewer.ws.send(JSON.stringify({ type: 'clay_navigation_offer', deliveryId: id }));
43
70
  if (!alive(viewer.ws, owner)) throw new Error('Viewer authorization or connection ended during delivery.');
@@ -48,30 +75,44 @@ function attachOwnerUi(ctx) {
48
75
  function read(owner, id) {
49
76
  var record = deliveries.get(id);
50
77
  if (!record || record.owner !== owner) throw new Error('No delivery exists for this owner.');
78
+ if (Date.now() - record.created > 300000) { deliveries.delete(id); throw new Error('The five-minute navigation receipt expired. Inspect the current view before any new reviewed request.'); }
51
79
  record.revalidate();
52
80
  return Object.assign({}, record.result);
53
81
  }
54
82
  function handleMessage(ws, msg) {
55
- if (['clay_ui_ready', 'clay_navigation_claim', 'clay_navigation_ack'].indexOf(msg.type) < 0) return false;
83
+ if (['clay_ui_ready', 'clay_navigation_claim', 'clay_navigation_ack', 'clay_navigation_rendered'].indexOf(msg.type) < 0) return false;
56
84
  var owner = ctx.isMultiUser() ? ws._clayUser && ws._clayUser.id : null;
57
85
  if (!sockets(owner).some(function (item) { return item.ws === ws; })) return true;
58
- if (msg.type === 'clay_ui_ready') { if (msg.version === 1) capabilities.set(ws, 1); return true; }
86
+ if (msg.type === 'clay_ui_ready') {
87
+ if (msg.version === 1) { capabilities.set(ws, 1); ws.send(JSON.stringify({ type: 'clay_control_viewer', viewerRef: identities.get(ws) })); }
88
+ return true;
89
+ }
59
90
  var record = deliveries.get(msg.deliveryId);
60
91
  if (!record || record.viewer.ws !== ws || record.owner !== owner || Date.now() - record.created > 30000) return true;
61
92
  try {
62
93
  record.revalidate();
63
- if (msg.type === 'clay_navigation_claim') {
94
+ if (msg.type === 'clay_navigation_rendered') {
95
+ if (!record.renderClosed && Date.now() > record.created + (ctx.renderTimeoutMs || 5000)) { record.renderClosed = true; clearTimeout(record.renderTimer); record.result.renderStatus = 'unknown'; if (record.activity) record.activity.navigation('unknown'); }
96
+ if (record.renderClosed || !record.claimed || !record.result.acknowledged || record.result.clientStatus !== 'accepted' || msg.renderToken !== record.renderToken) return true;
97
+ var current = inspect(owner, record.viewer.ref);
98
+ if (current.ws !== ws || msg.sessionOriginId !== record.target.sessionOriginId || !record.target.sessionOriginId || msg.projectSlug !== record.target.projectSlug || msg.sessionId !== record.target.sessionId || msg.surface !== 'conversation' || msg.entryIndex !== record.target.entryIndex || current.slug !== record.target.projectSlug || current.sessionId !== record.target.sessionId) throw new Error('Rendered target or authenticated viewer changed.');
99
+ record.renderClosed = true; clearTimeout(record.renderTimer);
100
+ record.result.renderStatus = msg.visible === true ? 'rendered' : 'unknown'; record.result.visible = msg.visible === true ? true : 'unknown';
101
+ if (record.activity) record.activity.navigation(msg.visible === true ? 'rendered' : 'unknown');
102
+ } else if (msg.type === 'clay_navigation_claim') {
64
103
  if (record.claimed) return true;
65
104
  check(owner, record.viewer); record.claimed = true;
66
- ws.send(JSON.stringify({ type: 'clay_navigation_apply', deliveryId: msg.deliveryId, target: record.target }));
105
+ ws.send(JSON.stringify({ type: 'clay_navigation_apply', deliveryId: msg.deliveryId, target: record.target, renderToken: record.renderToken, renderDeadline: record.created + (ctx.renderTimeoutMs || 5000) }));
67
106
  } else if (record.claimed && !record.result.acknowledged && ['accepted', 'refused', 'failed'].indexOf(msg.status) >= 0) {
68
107
  record.result.acknowledged = true;
69
108
  // Accepted only confirms invocation of the existing navigator, never rendering.
70
109
  record.result.clientStatus = msg.status;
110
+ if (msg.status !== 'accepted') { record.renderClosed = true; clearTimeout(record.renderTimer); }
111
+ if (record.activity) record.activity.navigation(msg.status === 'accepted' ? 'acknowledged' : msg.status === 'refused' ? 'cancelled' : 'failed');
71
112
  }
72
- } catch (error) { record.result.status = 'failed'; record.result.error = error.message; record.result.retrySafe = false; }
113
+ } catch (error) { record.renderClosed = true; clearTimeout(record.renderTimer); record.result.visible = 'unknown'; record.result.status = 'failed'; record.result.error = error.message; record.result.retrySafe = false; if (record.activity) record.activity.cancel(); }
73
114
  return true;
74
115
  }
75
- return { list: sockets, inspect: inspect, check: check, deliver: deliver, read: read, handleMessage: handleMessage };
116
+ return { list: sockets, inspect: inspect, check: check, deliver: deliver, read: read, handleMessage: handleMessage, activity: activity };
76
117
  }
77
118
  module.exports = { attachOwnerUi: attachOwnerUi };
@@ -12,11 +12,13 @@ function attachSettings(ctx) {
12
12
  var CONFIG_DIR = ctx.CONFIG_DIR;
13
13
 
14
14
  var profilePath = path.join(CONFIG_DIR, "profile.json");
15
+ var contextualClaySettings = require('./server-contextual-clay-settings').attachContextualClaySettings(ctx);
15
16
  var appearanceSettings = require('./server-appearance-settings').attachAppearanceSettings(ctx);
16
17
  var notificationSettings = require('./server-notification-settings').attachNotificationSettings(ctx);
17
18
  var experimentalSettings = attachExperimentalSettings(ctx);
18
19
 
19
20
  function handleRequest(req, res, fullUrl) {
21
+ if (contextualClaySettings.handleRequest(req, res, fullUrl)) return true;
20
22
  if (appearanceSettings.handleRequest(req, res, fullUrl)) return true;
21
23
  if (notificationSettings.handleRequest(req, res, fullUrl)) return true;
22
24
  if (experimentalSettings.handleRequest(req, res, fullUrl)) return true;
package/lib/server.js CHANGED
@@ -357,6 +357,12 @@ function createServer(opts) {
357
357
 
358
358
  // --- Project registry ---
359
359
  var assignmentService = attachWorkspaceAssignmentService({
360
+ getPresentation: function (principal) {
361
+ var project = projects.get(principal.sourceProjectSlug), manager = project && project.getSessionManager(), status = project && project.getStatus();
362
+ var session = manager && manager.sessions.get(principal.sourceSessionId);
363
+ var bound = clayProductControl && session && clayProductControl.bind({ projectSlug: principal.sourceProjectSlug, projectOwnerId: status.projectOwnerId, isMate: status.isMate, mateId: status.mateId, session: session });
364
+ return bound && bound._presentation;
365
+ },
360
366
  storageDir: path.join(CONFIG_DIR, "workspace-assignments"),
361
367
  getProjects: function () { return projects; },
362
368
  isMultiUser: function () { return users.isMultiUser(); },
@@ -384,6 +390,13 @@ function createServer(opts) {
384
390
  projects.forEach(function (project, slug) { project.forEachClient(function (ws) { visit(ws, slug); }); });
385
391
  }
386
392
  });
393
+ var mateContextService = require('./mate-context-service').attachMateContextService({
394
+ getOwnerUi: function () { return ownerUiService; }, workspace: workspaceQueryService,
395
+ getProjects: function () { return projects; }, isMultiUser: users.isMultiUser,
396
+ findUser: users.findUserById, onGetProjectAccess: onGetProjectAccess,
397
+ canAccessProject: users.canAccessProject,
398
+ resolveMate: function (owner, id) { return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? owner : null), id); }
399
+ });
387
400
  var clayProductControl = require("./clay-product-control").attachClayProductControl({
388
401
  getOwnerUi: function () { return ownerUiService; },
389
402
  getNotifications: function () { return _globalNotifications; },
@@ -1284,6 +1297,7 @@ function createServer(opts) {
1284
1297
  if (!require('./account-auth-socket').guardSocket(ws, req, isRequestAuthed, auth.touchSession)) return;
1285
1298
  ws._clayPane = wsRequest.pane;
1286
1299
  ws._clayPaneSession = wsRequest.paneSession;
1300
+ ws._clayContextual = wsRequest.contextual;
1287
1301
  globalWs.handleConnection(ws, globalUser);
1288
1302
  });
1289
1303
  return;
@@ -1329,6 +1343,7 @@ function createServer(opts) {
1329
1343
  ws._clayUser = wsUser; // attach user context
1330
1344
  ws._clayPane = wsRequest.pane;
1331
1345
  ws._clayPaneSession = wsRequest.paneSession;
1346
+ ws._clayContextual = wsRequest.contextual;
1332
1347
  var remoteAddr = req.socket.remoteAddress || "";
1333
1348
  ws._clayLocal = (remoteAddr === "127.0.0.1" || remoteAddr === "::1" || remoteAddr === "::ffff:127.0.0.1");
1334
1349
  // Clear cross-project unread for this project when client connects
@@ -1450,6 +1465,7 @@ function createServer(opts) {
1450
1465
  },
1451
1466
  workspaceQueryService: workspaceQueryService,
1452
1467
  clayProductControl: clayProductControl,
1468
+ mateContextService: mateContextService,
1453
1469
  resumeLifecycleProject: function (userId) { return opts.projectLifecycle ? opts.projectLifecycle.resumeRecovered(slug, userId) : true; },
1454
1470
  projectLogsService: projectLogsService,
1455
1471
  issuesService: issuesService,
@@ -1729,6 +1745,7 @@ function createServer(opts) {
1729
1745
 
1730
1746
  function handleDmMessage(ws, msg, routedProjectSlug) {
1731
1747
  if (ownerUiService.handleMessage(ws, msg)) return true;
1748
+ if (mateContextService.handleMessage(ws, msg)) return true;
1732
1749
  if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
1733
1750
  if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
1734
1751
  if (contextViewService && contextViewService.handleMessage(ws, msg)) return true;
package/lib/sessions.js CHANGED
@@ -211,8 +211,11 @@ function createSessionManager(opts) {
211
211
  if (session.claySchedulingReceipts) metaObj.claySchedulingReceipts = session.claySchedulingReceipts;
212
212
  if (session.mateCreationMode) metaObj.mateCreationMode = true;
213
213
  if (session.homeMateCreationPhase) metaObj.homeMateCreationPhase = session.homeMateCreationPhase;
214
+ if (session.contextualCreationMateStamp) metaObj.contextualCreationMateStamp = session.contextualCreationMateStamp;
215
+ if (session.contextualCreationRequestId) metaObj.contextualCreationRequestId = session.contextualCreationRequestId;
214
216
  if (session.homeClayEntryRequestId) metaObj.homeClayEntryRequestId = session.homeClayEntryRequestId;
215
217
  if (session.homeClayEntryMode) metaObj.homeClayEntryMode = session.homeClayEntryMode;
218
+ if (session.homeClayEntryText) metaObj.homeClayEntryText = session.homeClayEntryText;
216
219
  metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
217
220
  if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
218
221
  if (Array.isArray(session._workerGenerations)) metaObj.workerGenerations = session._workerGenerations;
@@ -375,8 +378,11 @@ function createSessionManager(opts) {
375
378
  if (Array.isArray(m.claySchedulingReceipts)) session.claySchedulingReceipts = m.claySchedulingReceipts;
376
379
  if (m.mateCreationMode) session.mateCreationMode = true;
377
380
  if (m.homeMateCreationPhase) session.homeMateCreationPhase = m.homeMateCreationPhase;
381
+ if (m.contextualCreationMateStamp) session.contextualCreationMateStamp = m.contextualCreationMateStamp;
382
+ if (m.contextualCreationRequestId) session.contextualCreationRequestId = m.contextualCreationRequestId;
378
383
  if (m.homeClayEntryRequestId) session.homeClayEntryRequestId = m.homeClayEntryRequestId;
379
384
  if (m.homeClayEntryMode) session.homeClayEntryMode = m.homeClayEntryMode;
385
+ if (m.homeClayEntryText) session.homeClayEntryText = m.homeClayEntryText;
380
386
  if (m.ownerId) session.ownerId = m.ownerId;
381
387
  session.sessionOriginId = m.sessionOriginId || null;
382
388
  session._provenanceMigrationNeeded = sessionProvenance.restore(m, session);
@@ -561,6 +567,7 @@ function createSessionManager(opts) {
561
567
  var unreadMap = wsUnread || singleUserUnread;
562
568
  return {
563
569
  id: s.localId,
570
+ sessionOriginId: s.sessionOriginId || null,
564
571
  cliSessionId: s.cliSessionId || null,
565
572
  createdAt: s.createdAt || 0,
566
573
  title: s.title || "New Session",
@@ -771,8 +778,8 @@ function createSessionManager(opts) {
771
778
  }
772
779
 
773
780
  // History entries include tool activity and streamed output, so opening a
774
- // conversation needs a wider window than each incremental scroll-up page.
775
- var INITIAL_HISTORY_SIZE = 500;
781
+ // conversation starts with a 300-entry target, aligned to a complete user turn.
782
+ var INITIAL_HISTORY_SIZE = 300;
776
783
  var HISTORY_PAGE_SIZE = 100;
777
784
 
778
785
  function findTurnBoundary(history, targetIndex) {
@@ -826,7 +833,7 @@ function createSessionManager(opts) {
826
833
  }
827
834
  }
828
835
 
829
- _send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, context: lastContext, contextUsage: require("./yoke/usage").context(session.lastContextUsage, session.vendor || "claude") });
836
+ _send({ type: "history_done", controlHistoryRevision: require("./clay-control-target-projection").digest(session.history || []), sessionId: session.localId, sessionOriginId: session.sessionOriginId || null, lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, context: lastContext, contextUsage: require("./yoke/usage").context(session.lastContextUsage, session.vendor || "claude") });
830
837
  for (var vi = 0; vi < sessionViewedListeners.length; vi++) {
831
838
  try { sessionViewedListeners[vi](session, targetWs, _send); } catch (e) {}
832
839
  }
@@ -83,7 +83,7 @@ function createTerminalManager(opts) {
83
83
  }
84
84
 
85
85
  // Broadcast to subscribers
86
- var msg = JSON.stringify({ type: "term_output", id: id, data: data });
86
+ var msg = JSON.stringify(Object.assign({ type: "term_output", id: id, data: data }, controlProjection(id)));
87
87
  for (var ws of session.subscribers) {
88
88
  if (ws.readyState === 1) ws.send(msg);
89
89
  }
@@ -108,7 +108,7 @@ function createTerminalManager(opts) {
108
108
  }
109
109
 
110
110
  // Broadcast updated list
111
- send({ type: "term_list", terminals: list() });
111
+ send({ type: "term_list", terminals: list(true) });
112
112
 
113
113
  // Caller-supplied hook: e.g. TUI session manager uses this to delete
114
114
  // its session record once `claude` exits, so stale entries don't pile
@@ -181,10 +181,11 @@ function createTerminalManager(opts) {
181
181
  session.cols = cols;
182
182
  session.rows = rows;
183
183
  // Notify other subscribers about the resize so their xterm stays in sync
184
- var msg = JSON.stringify({ type: "term_resized", id: id, cols: cols, rows: rows });
184
+ var msg = JSON.stringify(Object.assign({ type: "term_resized", id: id, cols: cols, rows: rows }, controlProjection(id)));
185
185
  for (var ws of session.subscribers) {
186
186
  if (ws.readyState === 1 && ws !== sourceWs) ws.send(msg);
187
187
  }
188
+ if (sourceWs && sourceWs.readyState === 1) sourceWs.send(JSON.stringify(Object.assign({ type: "term_control_projection", id: id }, controlProjection(id))));
188
189
  } catch (e) {}
189
190
  }
190
191
  }
@@ -244,15 +245,24 @@ function createTerminalManager(opts) {
244
245
  session.title = String(title).substring(0, 50);
245
246
  }
246
247
 
247
- function list() {
248
+ function inspect(id) {
249
+ var terminal = terminals.get(id);
250
+ return terminal ? { id: id, identity: terminal.identity, ownerId: terminal.ownerId, osIdentity: terminal.osIdentity, title: terminal.title, kind: terminal.kind, exited: terminal.exited, cols: terminal.cols, rows: terminal.rows, totalBytesWritten: terminal.totalBytesWritten } : null;
251
+ }
252
+
253
+ function controlProjection(id) { var item = inspect(id); return item ? { controlIdentity: item.identity, controlRevision: require('./clay-control-target-projection').digest(item) } : {}; }
254
+
255
+ function list(control) {
248
256
  var result = [];
249
257
  for (var session of terminals.values()) {
250
- result.push({
258
+ var entry = {
251
259
  id: session.id,
252
260
  title: session.title,
253
261
  kind: session.kind,
254
262
  exited: session.exited,
255
- });
263
+ };
264
+ if (control) { entry.controlIdentity = session.identity; entry.controlRevision = require('./clay-control-target-projection').digest(inspect(session.id)); }
265
+ result.push(entry);
256
266
  }
257
267
  return result;
258
268
  }
@@ -333,10 +343,7 @@ function createTerminalManager(opts) {
333
343
  resize: resize,
334
344
  close: close,
335
345
  closeAndWait: closeAndWait,
336
- inspect: function (id) {
337
- var terminal = terminals.get(id);
338
- return terminal ? { id: id, identity: terminal.identity, ownerId: terminal.ownerId, osIdentity: terminal.osIdentity, title: terminal.title, kind: terminal.kind, exited: terminal.exited, cols: terminal.cols, rows: terminal.rows, totalBytesWritten: terminal.totalBytesWritten } : null;
339
- },
346
+ inspect: inspect,
340
347
  resizeExact: function (id, cols, rows, identity, revalidate) {
341
348
  revalidate();
342
349
  var terminal = terminals.get(id);
@@ -345,7 +352,7 @@ function createTerminalManager(opts) {
345
352
  revalidate();
346
353
  if (terminals.get(id) !== terminal) throw new Error('Terminal was replaced during resize.');
347
354
  terminal.cols = cols; terminal.rows = rows;
348
- var message = JSON.stringify({ type: 'term_resized', id: id, cols: cols, rows: rows });
355
+ var message = JSON.stringify(Object.assign({ type: 'term_resized', id: id, cols: cols, rows: rows }, controlProjection(id)));
349
356
  terminal.subscribers.forEach(function (ws) { if (ws.readyState === 1) ws.send(message); });
350
357
  return true;
351
358
  },
@@ -0,0 +1,31 @@
1
+ // Personal geometry is never a canonical product-control operation.
2
+ function validGeometry(value) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
4
+ var keys = Object.keys(value);
5
+ if (keys.length !== 4 || keys.some(function (key) { return ['left', 'top', 'width', 'height'].indexOf(key) < 0; })) return false;
6
+ return keys.every(function (key) { return typeof value[key] === 'number' && Number.isFinite(value[key]); }) &&
7
+ value.left >= 0 && value.left <= 16384 && value.top >= 0 && value.top <= 16384 &&
8
+ value.width >= 340 && value.width <= 2560 && value.height >= 360 && value.height <= 2160;
9
+ }
10
+ function attachContextualClayPreferences(deps) {
11
+ function get(userId) {
12
+ var user = deps.loadUsers().users.find(function (entry) { return entry.id === userId; });
13
+ var saved = user && user.contextualClayGeometry;
14
+ if (!user && userId === 'default') saved = require('./config').loadConfig().contextualClayGeometry;
15
+ return validGeometry(saved) ? Object.assign({}, saved) : null;
16
+ }
17
+ function set(userId, geometry) {
18
+ if (!validGeometry(geometry)) return { error: 'Invalid popover geometry.' };
19
+ var value = Object.assign({}, geometry);
20
+ var data = deps.loadUsers();
21
+ var user = data.users.find(function (entry) { return entry.id === userId; });
22
+ if (user) { user.contextualClayGeometry = value; deps.saveUsers(data); }
23
+ else if (userId === 'default') {
24
+ var config = require('./config');
25
+ config.saveConfig(Object.assign({}, config.loadConfig(), { contextualClayGeometry: value }));
26
+ } else return { error: 'Account no longer available.' };
27
+ return { geometry: value };
28
+ }
29
+ return { getContextualClayGeometry: get, setContextualClayGeometry: set };
30
+ }
31
+ module.exports = { attachContextualClayPreferences: attachContextualClayPreferences, validGeometry: validGeometry };