clay-server 5.3.0-beta.5 → 5.3.0-beta.7

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 (185) hide show
  1. package/lib/clay-product-control-tools.js +2 -2
  2. package/lib/clay-product-control.js +1 -1
  3. package/lib/clay-screen-controls.js +1 -0
  4. package/lib/clay-split-view-awareness.js +5 -2
  5. package/lib/clay-split-view-controls.js +14 -7
  6. package/lib/clay-split-view-input.js +5 -4
  7. package/lib/clay-split-view-render-proof.js +14 -4
  8. package/lib/clay-view-workspace-input.js +71 -0
  9. package/lib/project-admin-overview.js +22 -11
  10. package/lib/project-background-tasks.js +76 -0
  11. package/lib/project-connection.js +1 -1
  12. package/lib/project-file-history.js +11 -9
  13. package/lib/project-file-watch.js +26 -7
  14. package/lib/project-filesystem.js +5 -4
  15. package/lib/project-sessions.js +12 -5
  16. package/lib/project-shared-browser.js +7 -3
  17. package/lib/project-split-view-document.js +16 -2
  18. package/lib/project-split-views.js +27 -297
  19. package/lib/project-user-message.js +4 -3
  20. package/lib/project-vendor-login.js +2 -2
  21. package/lib/project-view-input.js +11 -0
  22. package/lib/project-view-pair.js +26 -0
  23. package/lib/project-view-store.js +348 -0
  24. package/lib/project-view-terminal-binding.js +16 -0
  25. package/lib/project-view-workspace.js +126 -0
  26. package/lib/project-worker-conversation.js +5 -0
  27. package/lib/project.js +1 -0
  28. package/lib/public/app.js +9 -0
  29. package/lib/public/css/contextual-clay.css +2 -1
  30. package/lib/public/css/issues.css +3 -0
  31. package/lib/public/css/mate-conversation.css +1 -1
  32. package/lib/public/css/pane.css +5 -1
  33. package/lib/public/css/pending-message-queue.css +24 -19
  34. package/lib/public/css/project-admin-overview.css +42 -13
  35. package/lib/public/css/right-workbench.css +0 -1
  36. package/lib/public/css/scheduled-tasks.css +7 -0
  37. package/lib/public/css/shared-browser.css +31 -8
  38. package/lib/public/css/split-view-layout.css +129 -10
  39. package/lib/public/css/tool-pane.css +124 -0
  40. package/lib/public/index.html +6 -6
  41. package/lib/public/modules/agent-message-bubbles.js +96 -0
  42. package/lib/public/modules/app-header.js +6 -0
  43. package/lib/public/modules/app-messages.js +26 -9
  44. package/lib/public/modules/background-task-previews.js +8 -0
  45. package/lib/public/modules/background-task-viewer.js +97 -0
  46. package/lib/public/modules/background-tasks-ui.js +34 -125
  47. package/lib/public/modules/chat-bubble-renderer.js +11 -5
  48. package/lib/public/modules/clay-file-links.js +2 -1
  49. package/lib/public/modules/clay-split-view-screen.js +6 -3
  50. package/lib/public/modules/clay-tool-pane-screen.js +35 -0
  51. package/lib/public/modules/contextual-clay-geometry.js +7 -3
  52. package/lib/public/modules/file-history-snapshot.js +12 -0
  53. package/lib/public/modules/file-pane-entrypoints.js +55 -0
  54. package/lib/public/modules/file-pane-history.js +78 -0
  55. package/lib/public/modules/file-pane-instances.js +140 -0
  56. package/lib/public/modules/file-pane-links.js +41 -0
  57. package/lib/public/modules/file-pane-render.js +48 -0
  58. package/lib/public/modules/file-pane-review.js +26 -0
  59. package/lib/public/modules/file-pane-toolbar.js +54 -0
  60. package/lib/public/modules/file-viewer-source.js +70 -0
  61. package/lib/public/modules/filebrowser.js +43 -82
  62. package/lib/public/modules/git-panel.js +5 -0
  63. package/lib/public/modules/issues.js +3 -4
  64. package/lib/public/modules/linear-panel.js +2 -2
  65. package/lib/public/modules/mate-knowledge-workbench.js +10 -6
  66. package/lib/public/modules/notifications.js +4 -1
  67. package/lib/public/modules/pane-activation.js +7 -3
  68. package/lib/public/modules/pane-binding.js +5 -1
  69. package/lib/public/modules/pane-browser-bridge.js +6 -2
  70. package/lib/public/modules/pane-browser-transport.js +110 -0
  71. package/lib/public/modules/pane-header-tooltips.js +32 -0
  72. package/lib/public/modules/pending-message-queue-drag.js +69 -0
  73. package/lib/public/modules/pending-message-queue.js +3 -33
  74. package/lib/public/modules/permission-control.js +6 -5
  75. package/lib/public/modules/plantuml-preview.js +12 -12
  76. package/lib/public/modules/project-admin-overview.js +125 -10
  77. package/lib/public/modules/project-grouping.js +9 -0
  78. package/lib/public/modules/right-workbench.js +34 -15
  79. package/lib/public/modules/scheduled-tasks.js +7 -6
  80. package/lib/public/modules/server-settings-controls.js +33 -4
  81. package/lib/public/modules/session-folder-toolbar.js +1 -1
  82. package/lib/public/modules/session-tool-item.js +86 -0
  83. package/lib/public/modules/settings-select.js +2 -1
  84. package/lib/public/modules/shared-browser-tabs.js +37 -21
  85. package/lib/public/modules/shared-browser.js +69 -9
  86. package/lib/public/modules/sidebar-projects.js +1 -10
  87. package/lib/public/modules/split-pane-renderer.js +1 -0
  88. package/lib/public/modules/split-session-empty-pane.js +47 -0
  89. package/lib/public/modules/split-view-create-menu.js +74 -65
  90. package/lib/public/modules/split-view-drag-preview.js +90 -0
  91. package/lib/public/modules/split-view-layout-client.js +1 -1
  92. package/lib/public/modules/split-view-layout-scope.js +4 -0
  93. package/lib/public/modules/split-view-picker-actions.js +71 -0
  94. package/lib/public/modules/split-view-picker-render.js +81 -0
  95. package/lib/public/modules/split-view-presentation-drag.js +36 -13
  96. package/lib/public/modules/split-view-presentation-drop.js +21 -18
  97. package/lib/public/modules/split-view-presentation-renderer.js +22 -17
  98. package/lib/public/modules/split-view-presentations.js +25 -33
  99. package/lib/public/modules/split-view-worker-activity.js +2 -2
  100. package/lib/public/modules/split-view-worker-transcript.js +70 -33
  101. package/lib/public/modules/split-view-workers.js +6 -4
  102. package/lib/public/modules/split-view.js +9 -2
  103. package/lib/public/modules/sticky-notes-browser.js +2 -2
  104. package/lib/public/modules/terminal-viewport.js +14 -0
  105. package/lib/public/modules/terminal.js +49 -23
  106. package/lib/public/modules/theme-palette.js +177 -0
  107. package/lib/public/modules/theme.js +2 -180
  108. package/lib/public/modules/tool-pane-chrome.js +88 -0
  109. package/lib/public/modules/tool-pane-drag.js +134 -0
  110. package/lib/public/modules/tool-pane-layout.js +95 -0
  111. package/lib/public/modules/tool-pane-legacy.js +60 -0
  112. package/lib/public/modules/tool-pane-tree.js +51 -0
  113. package/lib/public/modules/tools.js +5 -89
  114. package/lib/public/modules/tooltip.js +32 -6
  115. package/lib/public/modules/view-legacy-coordinates.js +16 -0
  116. package/lib/public/modules/view-mutations.js +24 -0
  117. package/lib/public/modules/view-pair-presentation.js +74 -0
  118. package/lib/public/modules/view-pair-validation.js +12 -0
  119. package/lib/public/modules/view-presentation-state.js +37 -0
  120. package/lib/public/modules/view-workspace-controller.js +210 -0
  121. package/lib/public/modules/view-workspace-state.js +103 -0
  122. package/lib/public/modules/view-workspace-validation.js +47 -0
  123. package/lib/public/modules/wireframe-workbench.js +13 -2
  124. package/lib/public/modules/workbench-layout-preference.js +12 -5
  125. package/lib/public/modules/workbench-panels.js +21 -0
  126. package/lib/public/modules/worker-message-previews.js +39 -0
  127. package/lib/public/pane-assets/app-5W6KJXK5.js +1 -0
  128. package/lib/public/pane-assets/chunk-2IJUTAB6.js +2 -0
  129. package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
  130. package/lib/public/pane-assets/chunk-7OVNC5HT.js +1 -0
  131. package/lib/public/pane-assets/chunk-BQN7B62H.js +3 -0
  132. package/lib/public/pane-assets/chunk-EMLN2Y7L.js +1 -0
  133. package/lib/public/pane-assets/chunk-ENPAHXLB.js +1 -0
  134. package/lib/public/pane-assets/chunk-EPBGGZRT.js +1 -0
  135. package/lib/public/pane-assets/chunk-EYTE56IA.js +1 -0
  136. package/lib/public/pane-assets/chunk-FQTNR4ZS.js +1 -0
  137. package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
  138. package/lib/public/pane-assets/chunk-L6LSA5WI.js +1 -0
  139. package/lib/public/pane-assets/chunk-M57UIGZV.js +1 -0
  140. package/lib/public/pane-assets/chunk-MGTFTEJP.js +1 -0
  141. package/lib/public/pane-assets/chunk-MMG6MYCN.js +1 -0
  142. package/lib/public/pane-assets/chunk-MZLMEXMW.js +1 -0
  143. package/lib/public/pane-assets/chunk-OG3E5SVA.js +166 -0
  144. package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
  145. package/lib/public/pane-assets/chunk-RGAFZ3T2.js +1 -0
  146. package/lib/public/pane-assets/chunk-RNDRURMF.js +1 -0
  147. package/lib/public/pane-assets/chunk-SJOT74VW.js +2 -0
  148. package/lib/public/pane-assets/chunk-SJTGHIN5.js +1 -0
  149. package/lib/public/pane-assets/chunk-SLMAFKI2.js +1 -0
  150. package/lib/public/pane-assets/chunk-U3I5KY3B.js +1 -0
  151. package/lib/public/pane-assets/chunk-UPXU2IPC.js +1 -0
  152. package/lib/public/pane-assets/chunk-VEBYD7TL.js +1 -0
  153. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
  154. package/lib/public/pane-assets/chunk-XBHYNXXT.js +1 -0
  155. package/lib/public/pane-assets/manifest.json +438 -381
  156. package/lib/public/pane-assets/style-BSKKK54Q.css +1 -0
  157. package/lib/public/style.css +1 -0
  158. package/lib/sdk-message-processor.js +5 -2
  159. package/lib/server-storage.js +27 -3
  160. package/lib/server-workbench-layout-settings.js +3 -3
  161. package/lib/server.js +4 -2
  162. package/lib/sessions.js +1 -1
  163. package/lib/split-view-layout.js +11 -7
  164. package/lib/storage-usage.js +29 -7
  165. package/lib/terminal-manager.js +2 -2
  166. package/lib/users-workbench-layout-preferences.js +14 -4
  167. package/lib/workbench-window-layout.js +30 -0
  168. package/lib/worker-message-previews.js +50 -0
  169. package/lib/ws-schema.js +1 -1
  170. package/lib/yoke/adapters/claude-worker.js +8 -0
  171. package/lib/yoke/adapters/claude.js +17 -2
  172. package/lib/yoke/claude-task-reader.js +94 -0
  173. package/lib/yoke/interface.js +1 -0
  174. package/package.json +1 -1
  175. package/lib/public/pane-assets/app-6H5S2Y3Z.js +0 -1
  176. package/lib/public/pane-assets/chunk-3QV5SDC4.js +0 -1
  177. package/lib/public/pane-assets/chunk-4FM3U3DW.js +0 -1
  178. package/lib/public/pane-assets/chunk-7GZH3FZY.js +0 -1
  179. package/lib/public/pane-assets/chunk-AGJTF5WQ.js +0 -1
  180. package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
  181. package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
  182. package/lib/public/pane-assets/chunk-NPKRTUNG.js +0 -2
  183. package/lib/public/pane-assets/chunk-VGA2Y3D2.js +0 -2
  184. package/lib/public/pane-assets/chunk-WKO3T75E.js +0 -167
  185. package/lib/public/pane-assets/style-TSRDU54Z.css +0 -1
@@ -31,8 +31,7 @@ function attachFileWatch(ctx) {
31
31
  }
32
32
 
33
33
  // --- File watcher ---
34
- // One open file per websocket client. A project-wide singleton watcher made
35
- // one browser tab silently replace another tab's live preview subscription.
34
+ // Bounded instance subscriptions belong to the authenticated socket.
36
35
  var fileWatchers = new Map();
37
36
 
38
37
  function settleFileWatchReady(entry, ready) {
@@ -76,6 +75,7 @@ function attachFileWatch(ctx) {
76
75
  var latest = fileWatchers.get(key);
77
76
  if (!latest || latest.relPath !== relPath) return;
78
77
  try {
78
+ if (latest.authority !== JSON.stringify([client && client._clayUser && client._clayUser.id || null, scopeFor(client)])) { closeFileWatch(key); return; }
79
79
  var snapshot = readFileSnapshot(client, relPath);
80
80
  if (!snapshot) return;
81
81
  if (latest.hasSnapshot && latest.content === snapshot.content && latest.size === snapshot.size) return;
@@ -84,6 +84,8 @@ function attachFileWatch(ctx) {
84
84
  latest.size = snapshot.size;
85
85
  sendFileChanged(client, {
86
86
  type: "fs_file_changed",
87
+ instanceId: latest.instanceId,
88
+ requestId: latest.requestId,
87
89
  path: relPath,
88
90
  content: snapshot.content,
89
91
  size: snapshot.size,
@@ -95,7 +97,7 @@ function attachFileWatch(ctx) {
95
97
  }
96
98
  }
97
99
 
98
- function startFileWatch(client, relPath) {
100
+ function startFileWatch(client, relPath, binding) {
99
101
  // Preserve the old single-argument API for callers outside the websocket
100
102
  // file browser. They share one legacy subscription.
101
103
  if (typeof relPath !== "string") {
@@ -104,9 +106,16 @@ function attachFileWatch(ctx) {
104
106
  }
105
107
  var absPath;
106
108
  try { absPath = checkedPath(relPath, scopeFor(client)); } catch (error) { return Promise.resolve(false); }
109
+ if (binding && (!/^[A-Za-z0-9_-]{1,128}$/.test(binding.instanceId || '') || typeof binding.requestId !== 'string' || binding.requestId.length > 128)) return Promise.resolve(false);
107
110
  var key = client || "_legacy";
111
+ if (binding) {
112
+ var entries = Array.from(fileWatchers.entries()).filter(function (pair) { return pair[1].client === client; });
113
+ var match = entries.find(function (pair) { return pair[1].instanceId === binding.instanceId; });
114
+ if (!match && entries.length >= 8) return Promise.resolve(false);
115
+ key = match ? match[0] : {};
116
+ }
108
117
  var existing = fileWatchers.get(key);
109
- if (existing && existing.relPath === relPath) return existing.ready;
118
+ if (existing && existing.relPath === relPath && existing.requestId === (binding ? binding.requestId : undefined)) return existing.ready;
110
119
  closeFileWatch(key);
111
120
 
112
121
  // Watch the parent directory rather than the file inode. Editors and agent
@@ -132,6 +141,10 @@ function attachFileWatch(ctx) {
132
141
  var ready = new Promise(function (resolve) { resolveReady = resolve; });
133
142
  var entry = {
134
143
  watcher: watcher,
144
+ client: client,
145
+ authority: JSON.stringify([client && client._clayUser && client._clayUser.id || null, scopeFor(client)]),
146
+ instanceId: binding ? binding.instanceId : undefined,
147
+ requestId: binding ? binding.requestId : undefined,
135
148
  relPath: relPath,
136
149
  debounce: null,
137
150
  reconcile: null,
@@ -158,7 +171,7 @@ function attachFileWatch(ctx) {
158
171
  publishFileChanged(key, client, relPath);
159
172
  if (fileWatchers.get(key) === entry) settleFileWatchReady(entry, true);
160
173
  });
161
- watcher.on("error", function () { closeFileWatch(key); });
174
+ watcher.on("error", function () { if (fileWatchers.get(key) === entry) closeFileWatch(key); });
162
175
  return ready;
163
176
  } catch (e) {
164
177
  closeFileWatch(key);
@@ -166,9 +179,15 @@ function attachFileWatch(ctx) {
166
179
  }
167
180
  }
168
181
 
169
- function stopFileWatch(client) {
182
+ function stopFileWatch(client, binding) {
170
183
  if (arguments.length > 0) {
171
- closeFileWatch(client || "_legacy");
184
+ Array.from(fileWatchers.entries()).forEach(function (pair) {
185
+ var entry = pair[1];
186
+ if (entry.client !== client) return;
187
+ if (binding && (entry.instanceId !== binding.instanceId || entry.requestId !== binding.requestId)) return;
188
+ closeFileWatch(pair[0]);
189
+ });
190
+ if (!binding) closeFileWatch(client || "_legacy");
172
191
  return;
173
192
  }
174
193
  var keys = Array.from(fileWatchers.keys());
@@ -45,13 +45,14 @@ function attachFilesystem(ctx) {
45
45
  // Resolve live authority before every operation; never trust message roles.
46
46
  if (/^fs_/.test(msg.type) && msg.type !== "fs_unwatch") {
47
47
  try {
48
+ if (msg.instanceId && (msg.projectSlug !== slug || (msg.accountId || null) !== (ws._clayUser && ws._clayUser.id || null))) throw new Error('File pane context changed');
48
49
  fileScope = access.fileScope(ws);
49
50
  if (/^fs_(list|read|write|watch)$/.test(msg.type)) {
50
51
  fileTarget = resolveFilePath(cwd, msg.type === "fs_list" ? msg.path || "." : msg.path, fileScope);
51
52
  }
52
53
  } catch (e) {
53
54
  var denied = fileError(e);
54
- sendTo(ws, { type: msg.type + "_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, ok: false, error: denied.message, errorCode: denied.code });
55
+ sendTo(ws, { type: msg.type + "_result", path: msg.path, instanceId: msg.instanceId, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, ok: false, error: denied.message, errorCode: denied.code });
55
56
  return true;
56
57
  }
57
58
  }
@@ -115,7 +116,7 @@ function attachFilesystem(ctx) {
115
116
 
116
117
  // --- fs_read ---
117
118
  if (msg.type === "fs_read") {
118
- var metadata = { type: 'fs_read_result', path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId };
119
+ var metadata = { type: 'fs_read_result', path: msg.path, instanceId: msg.instanceId, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId };
119
120
  try {
120
121
  var readResult = operations.read(fileScope, msg.path);
121
122
  try { var fileSnapshot = operations.snapshot(fileScope, msg.path); if (fileSnapshot.hash === require('crypto').createHash('sha256').update(readResult.content).digest('hex')) readResult.controlRevision = require('./clay-control-target-projection').digest(fileSnapshot); } catch (ignored) {}
@@ -210,12 +211,12 @@ function attachFilesystem(ctx) {
210
211
 
211
212
  // --- File watcher ---
212
213
  if (msg.type === "fs_watch") {
213
- if (msg.path) startFileWatch(ws, msg.path);
214
+ if (msg.path) startFileWatch(ws, msg.path, msg.instanceId ? { instanceId: msg.instanceId, requestId: msg.requestId } : null);
214
215
  return true;
215
216
  }
216
217
 
217
218
  if (msg.type === "fs_unwatch") {
218
- stopFileWatch(ws);
219
+ stopFileWatch(ws, msg.instanceId ? { instanceId: msg.instanceId, requestId: msg.requestId } : { instanceId: undefined, requestId: undefined });
219
220
  return true;
220
221
  }
221
222
 
@@ -93,6 +93,7 @@ function formatAskUserAnswerAsMessage(input, answers) {
93
93
  function attachSessions(ctx) {
94
94
  var mateDefaultsToken = {};
95
95
  var contextUsage = contextUsageModule.attachContextUsage(ctx);
96
+ var backgroundTaskReader = require("./project-background-tasks").attach(ctx);
96
97
  var cwd = ctx.cwd;
97
98
  var slug = ctx.slug;
98
99
  var isMate = ctx.isMate;
@@ -606,6 +607,7 @@ function attachSessions(ctx) {
606
607
  if (workerRemoval.handleMessage(ws, msg)) return true;
607
608
  if (serverSettingsMessages.handleServerSettings({ opts: opts, usersModule: usersModule, sendTo: sendTo, send: send }, ws, msg)) return true;
608
609
  if (contextUsage.handleMessage(ws, msg)) return true;
610
+ if (backgroundTaskReader.handleMessage(ws, msg)) return true;
609
611
 
610
612
  if (msg.type === "push_subscribe") {
611
613
  var _pushUserId = ws._clayUser ? ws._clayUser.id : null;
@@ -681,6 +683,9 @@ function attachSessions(ctx) {
681
683
  if (creationRequestId) sendTo(ws, { type: "new_session_result", requestId: creationRequestId, ok: false, error: text });
682
684
  else sendTo(ws, { type: "error", text: text });
683
685
  };
686
+ if (msg.activate === false && (!creationRequestId || isMate || ws._clayPane || msg.slug !== slug)) {
687
+ creationFailed("Inactive creation requires this exact project and an ordinary session request."); return true;
688
+ }
684
689
  // The same request id is created at most once per socket.
685
690
  if (creationRequestId) {
686
691
  if (!ws._createdRequests) ws._createdRequests = [];
@@ -723,7 +728,8 @@ function attachSessions(ctx) {
723
728
  // Reuse an existing blank GUI session instead of stacking another
724
729
  // one. A vendor-less blank is acceptable: nothing has happened in it,
725
730
  // so stamping the requested vendor is equivalent to a fresh create.
726
- var reusable = msg.forceNew === true ? null : sm.findReusableBlankSession({
731
+ var inactiveCreation = msg.activate === false && !!creationRequestId && !isMate && !ws._clayPane;
732
+ var reusable = msg.forceNew === true || inactiveCreation ? null : sm.findReusableBlankSession({
727
733
  vendor: sessionOpts.vendor || null,
728
734
  ownerId: sessionOpts.ownerId || null,
729
735
  });
@@ -734,9 +740,10 @@ function attachSessions(ctx) {
734
740
  sm.switchSession(reusable.localId, ws);
735
741
  newSess = reusable;
736
742
  } else {
737
- newSess = sm.createSession(sessionOpts, ws);
743
+ newSess = inactiveCreation ? sm.createSessionRaw(sessionOpts) : sm.createSession(sessionOpts, ws);
738
744
  }
739
- ws._clayActiveSession = newSess.localId;
745
+ if (!inactiveCreation) ws._clayActiveSession = newSess.localId;
746
+ else { sm.saveSessionFile(newSess); sm.broadcastSessionList(); }
740
747
  var folderPlaced = msg.folderId === undefined ? null : sessionFolders.placeNewSession(ws, newSess, msg.folderId, msg.folderSlug);
741
748
  if (msg.requestId) {
742
749
  var created = { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: true, sessionId: newSess.localId };
@@ -758,8 +765,8 @@ function attachSessions(ctx) {
758
765
  send({ type: "last_vendor", vendor: msg.vendor });
759
766
  }
760
767
  var nsPresKey = ws._clayUser ? ws._clayUser.id : "_default";
761
- if (!ws._clayPane) userPresence.setPresence(slug, nsPresKey, userPresence.sessionIdForPersistence(newSess), null);
762
- if (usersModule.isMultiUser() && !ws._clayPane) {
768
+ if (!inactiveCreation && !ws._clayPane) userPresence.setPresence(slug, nsPresKey, userPresence.sessionIdForPersistence(newSess), null);
769
+ if (!inactiveCreation && usersModule.isMultiUser() && !ws._clayPane) {
763
770
  broadcastPresence();
764
771
  }
765
772
  return true;
@@ -52,9 +52,13 @@ function attachSharedBrowser(ctx) {
52
52
  if (tabs.length > 1) throw new Error("Choose a browserId from status before operating a tab");
53
53
  return tabs[0] || null;
54
54
  }
55
+ function sendBrowserMessage(ws, session, message) {
56
+ var payload = require('./pane-session-transport').outbound(ws, message, session.localId);
57
+ if (payload) ctx.sendTo(ws, payload);
58
+ }
55
59
  function sendState(ws, session, record, present, focus) {
56
60
  if (!ws || ws.readyState !== 1 || !allowed(session, ws) || (record && !recordAllowed(record, ws))) return;
57
- ctx.sendTo(ws, { type: "shared_browser_state", sessionId: session.localId, browser: projection(record), browsers: sessionRecords(session).map(projection), present: !!present, focus: !!focus });
61
+ sendBrowserMessage(ws, session, { type: "shared_browser_state", sessionId: session.localId, browser: projection(record), browsers: sessionRecords(session).map(projection), present: !!present, focus: !!focus });
58
62
  }
59
63
  function broadcast(record, present) {
60
64
  ctx.clients.forEach(function (ws) {
@@ -110,7 +114,7 @@ function attachSharedBrowser(ctx) {
110
114
  record.frame = { type: "shared_browser_frame", sessionId: record.session.localId, browserId: record.id,
111
115
  data: message.data, width: record.width, height: record.height };
112
116
  viewers.forEach(function (id, ws) {
113
- if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) ctx.sendTo(ws, record.frame);
117
+ if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) sendBrowserMessage(ws, record.session, record.frame);
114
118
  });
115
119
  visibility(record);
116
120
  } else broadcast(record);
@@ -287,7 +291,7 @@ function attachSharedBrowser(ctx) {
287
291
  record = selectRecord(session, msg.browserId);
288
292
  if (!record || !recordAllowed(record, ws) || msg.browserId !== record.id) throw new Error("Browser session expired");
289
293
  if (msg.type === "shared_browser_view") {
290
- if (msg.visible) { var previous = records.get(viewers.get(ws)); viewers.set(ws, record.id); if (previous && previous !== record) visibility(previous); if (record.frame) ctx.sendTo(ws, record.frame); }
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); }
291
295
  else viewers.delete(ws);
292
296
  visibility(record);
293
297
  } else if (msg.type === "shared_browser_control") {
@@ -1,9 +1,11 @@
1
1
  var layout = require("./split-view-layout");
2
+ var workspace = require("./project-view-workspace");
2
3
 
3
4
  function validDocument(value) {
4
5
  if (!value || value.version !== 1 || !value.projects || typeof value.projects !== "object" || Array.isArray(value.projects)) return false;
5
6
  var projectSlugs = Object.keys(value.projects);
6
7
  for (var i = 0; i < projectSlugs.length; i++) {
8
+ if (["__proto__", "constructor", "prototype"].indexOf(projectSlugs[i]) >= 0) return false;
7
9
  var project = value.projects[projectSlugs[i]];
8
10
  if (!project || !project.owners || typeof project.owners !== "object" || Array.isArray(project.owners)) return false;
9
11
  var ownerKeys = Object.keys(project.owners);
@@ -11,7 +13,7 @@ function validDocument(value) {
11
13
  var record = project.owners[ownerKeys[j]];
12
14
  if (!record || !(record.ownerId === null || typeof record.ownerId === "string") ||
13
15
  !(record.linuxUser === null || typeof record.linuxUser === "string") ||
14
- !Number.isInteger(record.revision) || record.revision < 0 || !Array.isArray(record.views)) return false;
16
+ !Number.isSafeInteger(record.revision) || record.revision < 0 || !Array.isArray(record.views)) return false;
15
17
  var ids = [], memberships = [];
16
18
  var ownerIdForKey = ownerKeys[j] === "default" ? null : ownerKeys[j].slice(5);
17
19
  if (ownerKeys[j] !== "default" && (ownerKeys[j].slice(0, 5) !== "user:" || !ownerIdForKey || ownerIdForKey !== record.ownerId)) return false;
@@ -25,9 +27,21 @@ function validDocument(value) {
25
27
  memberships.push(checked.origins[m]);
26
28
  }
27
29
  }
30
+ if (record.activeViewId != null && ids.indexOf(record.activeViewId) < 0) return false;
28
31
  }
29
32
  }
30
33
  return true;
31
34
  }
32
35
 
33
- module.exports = { validDocument: validDocument };
36
+ // Normalize copies only. Legacy records keep their original revision and bytes
37
+ // until an explicit mutation; no resource lookup or implicit migration write.
38
+ function normalizeDocument(value) {
39
+ if (!validDocument(value)) throw new TypeError('Invalid saved View document');
40
+ var result = JSON.parse(JSON.stringify(value));
41
+ Object.keys(result.projects).forEach(function (slug) {
42
+ var owners = result.projects[slug].owners;
43
+ Object.keys(owners).forEach(function (key) { owners[key].views = owners[key].views.map(workspace.normalized); });
44
+ });
45
+ return result;
46
+ }
47
+ module.exports = { validDocument: validDocument, normalizeDocument: normalizeDocument };