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
@@ -4,6 +4,23 @@ import { showToast } from './utils.js';
4
4
  import { confirmSettingsAction, clearSettingsDraft, setSettingsDraft } from './server-settings-state.js';
5
5
  import { enhanceSettingsSelects } from './settings-select.js';
6
6
 
7
+ var controlSettings = {
8
+ set_keep_awake: { config: 'keepAwake', field: 'value' },
9
+ set_image_retention: { config: 'imageRetentionDays', field: 'days' },
10
+ set_update_channel: { config: 'updateChannel', field: 'channel' }
11
+ };
12
+ function controlValue(input, type) {
13
+ if (type === 'set_image_retention') return Number(input.value);
14
+ if (type === 'set_update_channel') return input.checked ? 'beta' : 'stable';
15
+ return input.checked;
16
+ }
17
+ function savedControlValue(type) {
18
+ var value = (store.get('serverSettingsConfig') || {})[controlSettings[type].config];
19
+ if (type === 'set_image_retention') return value === undefined ? 7 : Number(value);
20
+ if (type === 'set_update_channel') return value === 'beta' ? 'beta' : 'stable';
21
+ return !!value;
22
+ }
23
+
7
24
  function status(id, text) {
8
25
  var el = document.getElementById(id);
9
26
  if (el) { el.textContent = text; el.classList.remove('hidden'); }
@@ -19,7 +36,7 @@ function sendSetting(type, values, buttonId, statusId) {
19
36
  if (input) { input.disabled = true; input.dispatchEvent(new Event('settings-sync')); }
20
37
  status(statusId, 'Saving…');
21
38
  var pending = Object.assign({}, store.get('serverSettingsPending') || {});
22
- pending[type] = { buttonId: buttonId, statusId: statusId };
39
+ pending[type] = { buttonId: buttonId, statusId: statusId, submitted: controlSettings[type] ? values[controlSettings[type].field] : null };
23
40
  store.set({ serverSettingsPending: pending });
24
41
  ws.send(JSON.stringify(Object.assign({ type: type }, values)));
25
42
  setTimeout(function () {
@@ -51,14 +68,25 @@ export function handleServerSettingResult(msg) {
51
68
  var input = request.buttonId === 'settings-pin-save-btn' ? document.getElementById('settings-pin-input') : request.buttonId.endsWith('-save') ? document.getElementById(request.buttonId.slice(0, -5)) : null;
52
69
  if (input) { input.disabled = false; input.dispatchEvent(new Event('settings-sync')); }
53
70
  status(request.statusId, msg.ok ? 'Saved. ' + (msg.timing || '') : (msg.error || 'Could not save. Retry your change.'));
54
- if (msg.ok) clearSettingsDraft(type);
71
+ if (msg.ok) {
72
+ var setting = controlSettings[type];
73
+ if (setting) {
74
+ var config = Object.assign({}, store.get('serverSettingsConfig') || {});
75
+ config[setting.config] = request.submitted;
76
+ store.set({ serverSettingsConfig: config });
77
+ var dirty = input && controlValue(input, type) !== savedControlValue(type);
78
+ setSettingsDraft(type, dirty);
79
+ if (button) button.hidden = !dirty;
80
+ if (dirty) status(request.statusId, 'Submitted value saved. Newer edits remain unsaved.');
81
+ } else clearSettingsDraft(type);
82
+ }
55
83
  }
56
84
 
57
85
  function finishSetting(msg, id, field) {
58
86
  handleServerSettingResult(msg);
59
87
  var el = document.getElementById(id);
60
88
  var key = 'set_keep_awake';
61
- if (!msg.type.endsWith('_result') && (store.get('serverSettingsDrafts') || {})[key]) return;
89
+ if ((store.get('serverSettingsDrafts') || {})[key]) return;
62
90
  if (el && msg.ok !== false && msg[field] !== undefined) el.checked = !!msg[field];
63
91
  }
64
92
 
@@ -108,7 +136,8 @@ export function initServerControls() {
108
136
  var save = document.createElement('button'); save.type = 'button'; save.className = 'settings-btn-sm'; save.textContent = 'Save changes';
109
137
  save.id = entry[0] + '-save'; save.hidden = true; receipt.insertAdjacentElement('beforebegin', save);
110
138
  input.onchange = function () {
111
- setSettingsDraft(entry[1], true); save.hidden = false; receipt.textContent = entry[3];
139
+ var dirty = controlValue(input, entry[1]) !== savedControlValue(entry[1]);
140
+ setSettingsDraft(entry[1], dirty); save.hidden = !dirty; receipt.textContent = dirty ? entry[3] : 'No unsaved changes';
112
141
  };
113
142
  save.onclick = function () {
114
143
  var values = {}; values[entry[2]] = entry[2] === 'days' ? Number(input.value) : entry[2] === 'channel' ? (input.checked ? 'beta' : 'stable') : input.checked;
@@ -165,7 +165,7 @@ export function renderFolderToolbar(surface, displayCounts) {
165
165
  view.setAttribute("aria-haspopup", "true");
166
166
  var viewState = store.get('sessionFolderViewMenu');
167
167
  view.setAttribute("aria-expanded", String(!!(viewState && viewState.surface === surface)));
168
- view.innerHTML = iconHtml("sliders-horizontal") + "<span>View</span>";
168
+ view.innerHTML = iconHtml("sliders-horizontal") + "<span>View options</span>";
169
169
  view.addEventListener("click", function () { toggleViewMenu(surface); });
170
170
 
171
171
  var magnifier = document.createElement("button");
@@ -0,0 +1,86 @@
1
+ import { clayToolGroupSummary } from './clay-tool-presentation.js';
2
+ import { iconHtml } from './icons.js';
3
+
4
+ // Shared stateless activity chrome for live sessions and read-only transcripts.
5
+ export function createSessionToolItem(id, name) {
6
+ var el = document.createElement("div");
7
+ el.className = "tool-item";
8
+ if (id != null) el.dataset.toolId = id;
9
+ el.innerHTML =
10
+ '<div class="tool-header">' +
11
+ '<span class="tool-chevron">' + iconHtml("chevron-right") + '</span>' +
12
+ '<span class="tool-bullet"></span>' +
13
+ '<span class="tool-name"></span>' +
14
+ '<span class="tool-desc"></span>' +
15
+ '<span class="tool-status-icon">' + iconHtml("loader", "icon-spin") + '</span>' +
16
+ '</div>' +
17
+ '<div class="tool-subtitle">' +
18
+ '<span class="tool-connector">&#9492;</span>' +
19
+ '<span class="tool-subtitle-text">Running...</span>' +
20
+ '</div>';
21
+
22
+ el.querySelector(".tool-name").textContent = name;
23
+
24
+ return el;
25
+ }
26
+
27
+ export function createSessionToolGroup(avatarUrl) {
28
+ var group = document.createElement('div'); group.className = 'tool-group';
29
+ var parent = group;
30
+ if (avatarUrl) {
31
+ var image = document.createElement('img'); image.className = 'dm-bubble-avatar dm-bubble-avatar-mate'; image.src = avatarUrl; image.alt = ''; group.appendChild(image);
32
+ parent = document.createElement('div'); parent.className = 'dm-bubble-content'; group.appendChild(parent);
33
+ }
34
+ parent.innerHTML = '<div class="tool-group-header" style="display:none"><span class="tool-group-chevron">' + iconHtml('chevron-right') + '</span><span class="tool-group-bullet"></span><span class="tool-group-label">Running...</span><span class="tool-group-status-icon">' + iconHtml('loader', 'icon-spin') + '</span></div><div class="tool-group-items"></div>';
35
+ return group;
36
+ }
37
+
38
+ export function sessionToolGroupSummary(group, tools) {
39
+ var claySummary = clayToolGroupSummary(group, tools);
40
+ if (claySummary) return claySummary;
41
+ var names = group.toolNames;
42
+ var count = names.length;
43
+ var allDone = group.doneCount >= count;
44
+
45
+ // Count by tool name
46
+ var counts = {};
47
+ for (var i = 0; i < names.length; i++) {
48
+ counts[names[i]] = (counts[names[i]] || 0) + 1;
49
+ }
50
+ var uniqueNames = Object.keys(counts);
51
+
52
+ if (uniqueNames.length === 1) {
53
+ var name = uniqueNames[0];
54
+ var n = counts[name];
55
+ if (allDone) {
56
+ switch (name) {
57
+ case "Read": return "Read " + n + " file" + (n > 1 ? "s" : "");
58
+ case "Edit": return "Edited " + n + " file" + (n > 1 ? "s" : "");
59
+ case "Write": return "Wrote " + n + " file" + (n > 1 ? "s" : "");
60
+ case "Bash": return "Ran " + n + " command" + (n > 1 ? "s" : "");
61
+ case "Grep": return "Searched " + n + " pattern" + (n > 1 ? "s" : "");
62
+ case "Glob": return "Found " + n + " pattern" + (n > 1 ? "s" : "");
63
+ case "Task": return "Ran " + n + " task" + (n > 1 ? "s" : "");
64
+ case "WebSearch": return "Searched " + n + " quer" + (n > 1 ? "ies" : "y");
65
+ case "WebFetch": return "Fetched " + n + " URL" + (n > 1 ? "s" : "");
66
+ default: return "Ran " + n + " tool" + (n > 1 ? "s" : "");
67
+ }
68
+ }
69
+ switch (name) {
70
+ case "Read": return "Reading " + n + " file" + (n > 1 ? "s" : "") + "...";
71
+ case "Edit": return "Editing " + n + " file" + (n > 1 ? "s" : "") + "...";
72
+ case "Write": return "Writing " + n + " file" + (n > 1 ? "s" : "") + "...";
73
+ case "Bash": return "Running " + n + " command" + (n > 1 ? "s" : "") + "...";
74
+ case "Grep": return "Searching " + n + " pattern" + (n > 1 ? "s" : "") + "...";
75
+ case "Glob": return "Finding " + n + " pattern" + (n > 1 ? "s" : "") + "...";
76
+ case "Task": return "Running " + n + " task" + (n > 1 ? "s" : "") + "...";
77
+ case "WebSearch": return "Searching " + n + " quer" + (n > 1 ? "ies" : "y") + "...";
78
+ case "WebFetch": return "Fetching " + n + " URL" + (n > 1 ? "s" : "") + "...";
79
+ default: return "Running " + n + " tool" + (n > 1 ? "s" : "") + "...";
80
+ }
81
+ }
82
+
83
+ // Mixed tools
84
+ if (allDone) return "Ran " + count + " tools";
85
+ return "Running " + count + " tools...";
86
+ }
@@ -55,8 +55,9 @@ export function enhanceSettingsSelects(root) {
55
55
  item.hidden = option.hidden;
56
56
  item.onclick = function (event) {
57
57
  event.preventDefault();
58
+ var changed = select.value !== option.value;
58
59
  select.value = option.value;
59
- select.dispatchEvent(new Event('change', { bubbles: true }));
60
+ if (changed) select.dispatchEvent(new Event('change', { bubbles: true }));
60
61
  sync();
61
62
  close(true);
62
63
  };
@@ -7,37 +7,53 @@ export function renderBrowserTabs(el, select, send) {
7
7
  var selected = store.get('sharedBrowser');
8
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')]);
9
9
  if (bar.dataset.key === key) return;
10
- bar.dataset.key = key;
10
+ var focused = bar.contains(document.activeElement) ? document.activeElement : null;
11
+ var focusedId = focused && focused.dataset.browserId, focusedAction = focused && focused.dataset.tabAction;
12
+ var previousSelection = bar.dataset.selected;
13
+ var previousStrip = bar.querySelector('.shared-browser-tab-list'), scrollLeft = previousStrip ? previousStrip.scrollLeft : 0;
14
+ bar.dataset.key = key; bar.dataset.selected = selected && selected.id || '';
11
15
  bar.replaceChildren();
16
+ var strip = document.createElement('div'); strip.className = 'shared-browser-tab-list'; strip.setAttribute('role', 'presentation');
17
+ bar.appendChild(strip);
12
18
  tabs.forEach(function (tab, index) {
13
- var item = document.createElement('div'); item.className = 'shared-browser-tab';
19
+ var item = document.createElement('div'); item.className = 'shared-browser-tab'; item.setAttribute('role', 'presentation');
20
+ var active = !!selected && selected.id === tab.id;
14
21
  var button = document.createElement('button'); button.type = 'button'; button.setAttribute('role', 'tab');
15
- button.setAttribute('aria-selected', String(!!selected && selected.id === tab.id));
16
- var title = 'Tab ' + (index + 1);
17
- try { if (tab.url) title = new URL(tab.url).hostname; } catch (e) {}
18
- var label = document.createElement('span'); label.textContent = title + (tab.personalCookiesAvailable && tab.incognito ? ' · Incognito' : '');
19
- var owner = document.createElement('small'); owner.textContent = tab.phase === 'ended' ? 'Ended' : tab.control === 'agent' ? 'Shared' : 'You only';
20
- button.append(label, owner); button.title = title + ' · ' + (tab.incognito === false ? 'Personal browser' : 'Incognito') + ' · ' + (tab.phase === 'ended' ? 'Ended' : tab.control === 'agent' ? 'You and Clay can control this tab' : 'Only you can control this tab. Clay is paused.');
22
+ button.dataset.browserId = tab.id; button.dataset.tabAction = 'select';
23
+ button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1;
24
+ var title = 'New tab';
25
+ try { if (tab.url) title = new URL(tab.url).hostname || 'New tab'; } catch (e) {}
26
+ 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;
28
+ var label = document.createElement('span'); label.className = 'shared-browser-tab-label'; label.textContent = title;
29
+ button.innerHTML = iconHtml(tab.personalCookiesAvailable && tab.incognito ? 'glasses' : 'globe');
30
+ button.appendChild(label); button.setAttribute('aria-label', description); button.setAttribute('data-tip', description);
31
+ item.dataset.phase = tab.phase; item.dataset.control = tab.control;
21
32
  button.onclick = function () { select(tab.id); };
22
33
  button.onkeydown = function (event) {
23
34
  var next = event.key === 'ArrowRight' ? (index + 1) % tabs.length : event.key === 'ArrowLeft' ? (index + tabs.length - 1) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : -1;
24
35
  if (next < 0) return;
25
36
  event.preventDefault(); select(tabs[next].id); bar.querySelectorAll('[role="tab"]')[next].focus();
26
37
  };
27
- var close = document.createElement('button'); close.type = 'button'; close.textContent = '×'; close.setAttribute('aria-label', 'Close browser tab ' + (index + 1)); close.disabled = !store.get('connected');
38
+ var close = document.createElement('button'); close.type = 'button'; close.className = 'shared-browser-tab-close'; close.innerHTML = iconHtml('x');
39
+ close.dataset.browserId = tab.id; close.dataset.tabAction = 'close';
40
+ close.setAttribute('aria-label', 'Close browser tab ' + (index + 1)); close.setAttribute('data-tip', 'Close tab'); close.disabled = !store.get('connected');
28
41
  close.onclick = function () { send('close', { browserId: tab.id }); };
29
- item.append(button, close); bar.appendChild(item);
42
+ item.append(button, close); strip.appendChild(item);
30
43
  });
31
- var add = document.createElement('button'); add.type = 'button'; add.textContent = '+'; add.setAttribute('aria-label', 'New browser tab'); add.disabled = !store.get('connected');
32
- add.title = selected && selected.personalCookiesAvailable ? 'New tab · Personal browser' : 'New incognito tab';
33
- add.onclick = function () { send('open', { newTab: true }); };
34
- bar.appendChild(add);
35
- if (selected && selected.personalCookiesAvailable) {
36
- var incognito = document.createElement('button'); incognito.type = 'button';
37
- incognito.innerHTML = iconHtml('glasses');
38
- incognito.setAttribute('aria-label', 'New incognito tab'); incognito.title = 'New incognito tab · Site data is not saved';
39
- incognito.disabled = !store.get('connected');
40
- incognito.onclick = function () { send('open', { newTab: true, incognito: true }); };
41
- bar.appendChild(incognito); refreshIcons();
44
+ function addButton(icon, label, description, fields, action) {
45
+ var button = document.createElement('button'); button.type = 'button'; button.className = 'shared-browser-new-tab';
46
+ button.innerHTML = iconHtml(icon); button.dataset.tabAction = action;
47
+ button.setAttribute('aria-label', label); button.setAttribute('data-tip', description); button.disabled = !store.get('connected');
48
+ button.onclick = function () { send('open', fields); }; bar.appendChild(button);
42
49
  }
50
+ addButton('plus', 'New browser tab', selected && selected.personalCookiesAvailable ? 'New tab · Personal browser' : 'New incognito tab', { newTab: true }, 'new');
51
+ if (selected && selected.personalCookiesAvailable) addButton('glasses', 'New incognito tab', 'New incognito tab · Site data is not saved', { newTab: true, incognito: true }, 'incognito');
52
+ refreshIcons();
53
+ strip.scrollLeft = scrollLeft;
54
+ var buttons = Array.from(bar.querySelectorAll('button'));
55
+ var replacement = focused && buttons.find(function (button) { return button.dataset.browserId === focusedId && button.dataset.tabAction === focusedAction; });
56
+ if (focused) (replacement || bar.querySelector('[aria-selected="true"]') || buttons[0]).focus({ preventScroll: true });
57
+ var current = bar.querySelector('[aria-selected="true"]');
58
+ if (current && previousSelection !== bar.dataset.selected) current.scrollIntoView({ block: 'nearest', inline: 'nearest' });
43
59
  }
@@ -1,4 +1,6 @@
1
+ import { recordToolExpansion } from './view-presentation-state.js';
1
2
  import { registerStudioPaint } from './clay-studio-render.js';
3
+ import { hasBrowserPaneHost, initBrowserPaneTransport, forwardBrowserPaneMessage, sendBrowserPaneCommand, ensureBrowserPaneSource, browserPaneSessionId, browserPaneStamp, browserPaneBound } from './pane-browser-transport.js';
2
4
  import { browserLinkTarget, initBrowserLinks } from './clay-browser-links.js';
3
5
  import { browserSettingsHtml, bindBrowserSettings, renderBrowserSettings, closeBrowserSettings, handleBrowserSettingsMessage } from './shared-browser-settings.js';
4
6
  import { renderBrowserTabs } from './shared-browser-tabs.js';
@@ -19,9 +21,11 @@ function eligible() { return visibleEntry() && !!store.get('activeSessionId'); }
19
21
  function panel() { return document.getElementById('shared-browser-panel'); }
20
22
  function ui(patch) { store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), patch) }); }
21
23
  function send(type, fields) {
24
+ var browser = store.get('sharedBrowser');
25
+ var paneSent = sendBrowserPaneCommand(type, Object.assign({ browserId: browser && browser.id, epoch: browser && browser.epoch }, fields || {}));
26
+ if (paneSent !== null) return paneSent;
22
27
  var ws = getWs();
23
28
  if (!ws || ws.readyState !== 1 || !store.get('activeSessionId')) return false;
24
- var browser = store.get('sharedBrowser');
25
29
  ws.send(JSON.stringify(Object.assign({ type: 'shared_browser_' + type, sessionId: store.get('activeSessionId'),
26
30
  browserId: browser && browser.id, epoch: browser && browser.epoch }, fields || {})));
27
31
  return true;
@@ -65,7 +69,7 @@ function ensurePanel() {
65
69
  root.appendChild(el);
66
70
  el.querySelector('[data-action="hide"]').onclick = hideSharedBrowser;
67
71
  el.querySelector('[data-action="wide"]').onclick = function () { var wide = !(store.get('sharedBrowserUi') || {}).wide; ui({ wide: wide }); render(); };
68
- el.querySelector('[data-action="fullscreen"]').onclick = function () { var full = !(store.get('sharedBrowserUi') || {}).fullscreen; ui({ fullscreen: full }); render(); };
72
+ el.querySelector('[data-action="fullscreen"]').onclick = function () { var full = !(store.get('sharedBrowserUi') || {}).fullscreen; setBrowserExpanded(full); recordToolExpansion('browser', full); };
69
73
  ['back', 'forward', 'reload'].forEach(function (action) { el.querySelector('[data-action="' + action + '"]').onclick = function () { sendBrowserInput({ kind: action }); }; });
70
74
  el.querySelector('form').onsubmit = function (event) { event.preventDefault(); sendBrowserInput({ kind: 'navigate', url: el.querySelector('form input').value }); };
71
75
  el.querySelector('[data-action="control"]').onclick = function () {
@@ -178,7 +182,28 @@ function selectBrowserTab(id) {
178
182
  setBrowserTab(browser);
179
183
  send('view', { visible: desktop() && !document.hidden }); render();
180
184
  }
185
+ function browserOpenContext() {
186
+ return JSON.stringify([store.get('myUserId'), store.get('currentSlug'), store.get('projectSplitViewActiveId'), store.get('sessionOriginId'), store.get('projectSplitViewSocketGeneration')]);
187
+ }
188
+ function sendOrQueueBrowserOpen(fields) {
189
+ store.set({ pendingBrowserOpen: null });
190
+ if (send('open', fields)) return;
191
+ // A false transport result means nothing was sent. Only this fresh human
192
+ // opening can wait for pane readiness; restoration never enters this path.
193
+ store.set({ pendingBrowserOpen: { context: browserOpenContext(), socket: getWs(), fields: fields || {}, expires: Date.now() + 15000 } });
194
+ }
195
+ function resumePendingBrowserOpen() {
196
+ var pending = store.get('pendingBrowserOpen');
197
+ if (!pending) return;
198
+ if (pending.context !== browserOpenContext() || pending.socket !== getWs() || pending.expires < Date.now() || !store.get('connected') || !store.get('sharedBrowserOpen')) {
199
+ store.set({ pendingBrowserOpen: null }); return;
200
+ }
201
+ if (!ensureBrowserPaneSource()) return;
202
+ store.set({ pendingBrowserOpen: null });
203
+ if (!send('open', pending.fields)) store.set({ pendingBrowserOpen: pending });
204
+ }
181
205
  export function openSharedBrowserAt(value) {
206
+ ensureBrowserPaneSource();
182
207
  var url = browserLinkTarget('browser:' + value);
183
208
  if (!url) throw new Error('Enter a valid HTTP or HTTPS browser address.');
184
209
  if (!desktop()) throw new Error('Clay Browser is available on desktop screens.');
@@ -186,19 +211,22 @@ export function openSharedBrowserAt(value) {
186
211
  if (!store.get('connected')) throw new Error('Reconnect before opening this browser link.');
187
212
  ensurePanel(); closeBrowserSettings(false);
188
213
  claimRightWorkbench('browser'); store.set({ sharedBrowserOpen: true });
189
- ui({ error: '' }); send('open', { url: url }); render();
214
+ ui({ error: '' }); sendOrQueueBrowserOpen({ url: url }); render();
190
215
  }
191
216
  export function openSharedBrowser() {
217
+ ensureBrowserPaneSource();
192
218
  if (!eligible() || !store.get('connected')) return;
193
219
  ensurePanel();
194
220
  claimRightWorkbench('browser');
195
221
  store.set({ sharedBrowserOpen: true });
196
222
  var browser = store.get('sharedBrowser');
197
- if (!browser) send('open');
223
+ if (!browser) sendOrQueueBrowserOpen();
198
224
  else send('view', { visible: true });
199
225
  render();
200
226
  }
201
227
  export function hideSharedBrowser() {
228
+ setBrowserExpanded(false); recordToolExpansion('browser', false);
229
+ if (!store.get('savedWorkspaceApplying')) store.set({ pendingBrowserOpen: null });
202
230
  closeViewportMenu();
203
231
  closeBrowserSettings();
204
232
  clearBrowserInput();
@@ -207,9 +235,11 @@ export function hideSharedBrowser() {
207
235
  releaseRightWorkbench('browser');
208
236
  render();
209
237
  }
210
- export function handleSharedBrowserMessage(msg) {
238
+ export function handleSharedBrowserMessage(msg, fromPane) {
211
239
  if (!msg.type || msg.type.indexOf('shared_browser_') !== 0) return false;
212
- if (String(msg.sessionId) !== String(store.get('activeSessionId'))) return true;
240
+ if (forwardBrowserPaneMessage(msg)) return true;
241
+ if (browserPaneBound() && !fromPane) return true;
242
+ if (String(msg.sessionId) !== String(browserPaneSessionId() || store.get('activeSessionId'))) return true;
213
243
  if (handleBrowserSettingsMessage(msg)) return true;
214
244
  if (msg.type === 'shared_browser_state') {
215
245
  var old = store.get('sharedBrowser');
@@ -218,6 +248,7 @@ export function handleSharedBrowserMessage(msg) {
218
248
  var selected = !msg.focus && old && tabs.find(function (tab) { return tab.id === old.id; });
219
249
  selected = selected || msg.browser || tabs[0] || null;
220
250
  setBrowserTab(selected);
251
+ store.set({ savedViewBrowserListReady: true });
221
252
  if (msg.present && eligible()) {
222
253
  ensurePanel(); claimRightWorkbench('browser'); store.set({ sharedBrowserOpen: true });
223
254
  }
@@ -228,23 +259,35 @@ export function handleSharedBrowserMessage(msg) {
228
259
  var current = store.get('sharedBrowser');
229
260
  if (!desktop() || !store.get('sharedBrowserOpen') || !current || current.id !== msg.browserId || (store.get('sharedBrowserUi') || {}).decoding) return true;
230
261
  var sessionId = store.get('activeSessionId');
262
+ var paneStamp = browserPaneStamp();
231
263
  ui({ decoding: true });
232
264
  var image = new Image();
233
265
  image.onload = function () {
234
266
  var latest = store.get('sharedBrowser');
235
- if (latest && latest.id === msg.browserId && store.get('activeSessionId') === sessionId && panel()) {
267
+ if (latest && latest.id === msg.browserId && browserPaneStamp() === paneStamp && store.get('activeSessionId') === sessionId && panel()) {
236
268
  var canvas = panel().querySelector('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
237
269
  canvas.getContext('2d').drawImage(image, 0, 0);
238
270
  ui({ decoding: false, hasFrame: true }); render();
239
271
  registerStudioPaint(msg, canvas, null, canvas);
240
272
  }
241
273
  };
242
- image.onerror = function () { if (store.get('activeSessionId') === sessionId) ui({ decoding: false }); };
274
+ image.onerror = function () { if (browserPaneStamp() === paneStamp && store.get('activeSessionId') === sessionId) ui({ decoding: false }); };
243
275
  image.src = 'data:image/jpeg;base64,' + msg.data;
244
276
  }
245
277
  return true;
246
278
  }
247
279
  export function initSharedBrowser() {
280
+ initBrowserPaneTransport(handleSharedBrowserMessage, function (available) {
281
+ var applying = store.get('savedWorkspaceApplying');
282
+ store.set({ savedWorkspaceApplying: true, savedViewBrowserListReady: false });
283
+ try {
284
+ clearBrowserInput(); setBrowserTab(null);
285
+ store.set({ sharedBrowsers: [], sharedBrowserUi: {}, sharedBrowserSettings: {} });
286
+ if (!available) hideSharedBrowser();
287
+ render();
288
+ } finally { store.set({ savedWorkspaceApplying: applying }); }
289
+ });
290
+ document.addEventListener('clay-pane-ready', function () { queueMicrotask(resumePendingBrowserOpen); }, true);
248
291
  initBrowserLinks();
249
292
  var button = document.getElementById('shared-browser-btn');
250
293
  if (button) button.onclick = openSharedBrowser;
@@ -261,14 +304,31 @@ export function initSharedBrowser() {
261
304
  registerRightWorkbench('browser', hideSharedBrowser);
262
305
  store.subscribe(function (state, previous) {
263
306
  if (state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.myUserId !== previous.myUserId) {
307
+ if (browserPaneSessionId() !== null) { render(); return; }
264
308
  clearBrowserInput(); releaseRightWorkbench('browser');
265
- store.set({ sharedBrowser: null, sharedBrowsers: [], sharedBrowserOpen: false, sharedBrowserUi: {}, sharedBrowserSettings: {} });
309
+ store.set({ sharedBrowser: null, sharedBrowsers: [], savedViewBrowserListReady: false, sharedBrowserOpen: false, sharedBrowserUi: {}, sharedBrowserSettings: {} });
266
310
  if (state.connected) send('state_request');
267
311
  render();
268
312
  } else if (state.connected !== previous.connected) {
313
+ store.set({ savedViewBrowserListReady: false });
269
314
  if (state.connected) send('state_request');
270
315
  render();
271
316
  } else if (state.permissions !== previous.permissions || state.dmMode !== previous.dmMode) {
272
317
  if (!eligible()) hideSharedBrowser(); renderEntry();
273
318
  }
274
319
  });
320
+
321
+ export function restoreExistingBrowser(resource) {
322
+ if (hasBrowserPaneHost() && !ensureBrowserPaneSource()) return 'pending';
323
+ if (!store.get('savedViewBrowserListReady')) { send('state_request'); return 'pending'; }
324
+ if (!store.get('connected') || !store.get('controlRuntimeId')) return 'pending';
325
+ ensurePanel();
326
+ var browser = (store.get('sharedBrowsers') || []).find(function (item) { return item.id === resource.id && item.phase !== 'ended'; });
327
+ var exact = browser && resource.runtimeId === store.get('controlRuntimeId') && resource.sessionOriginId === store.get('sessionOriginId');
328
+ claimRightWorkbench('browser'); store.set({ sharedBrowserOpen: true });
329
+ if (exact) { setBrowserTab(browser); send('view', { visible: true }); ui({ error: '' }); render(); return 'restored'; }
330
+ ui({ error: 'This saved browser is unavailable in the current conversation. No replacement browser was opened.' }); render();
331
+ return 'unavailable';
332
+ }
333
+
334
+ export function setBrowserExpanded(enabled) { ui({ fullscreen: !!enabled }); if (document.getElementById('shared-browser-panel')) render(); }
@@ -15,7 +15,7 @@ import { openAddProjectModal } from './project-start-dialog.js';
15
15
  import { showHomeHub } from './app-home-hub.js';
16
16
  import { getPendingTuiAttention } from './app-notifications.js';
17
17
  import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
18
- import { groupProjects } from './project-grouping.js';
18
+ import { groupProjects, getProjectAbbrev } from './project-grouping.js';
19
19
  import { showProjectAccessPopover } from './project-access.js';
20
20
  import { renderProjectMates, mateForWorkspace } from './project-mate-navigation.js';
21
21
  import { openProjectCreateChoice } from './project-mate-creation-workspace.js';
@@ -337,15 +337,6 @@ export function initSidebarProjects() {
337
337
  export function getCachedProjectList() { return cachedProjectList; }
338
338
  export function getCachedCurrentSlug() { return cachedCurrentSlug; }
339
339
 
340
- function getProjectAbbrev(name) {
341
- if (!name) return "?";
342
- var words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
343
- if (words.length >= 2) {
344
- return (words[0][0] + words[1][0]).toUpperCase();
345
- }
346
- return name.substring(0, 2).toUpperCase();
347
- }
348
-
349
340
  export { getProjectAbbrev };
350
341
 
351
342
  // --- Project context menu ---
@@ -296,6 +296,7 @@ export function createSplitPane(pane, closePane) {
296
296
  if (!vendorIcon.hidden) vendorIcon.src = VENDOR_AVATARS[vendor]; vendorIcon.alt = ""; vendorIcon.title = VENDOR_NAMES[vendor] || vendor; header.appendChild(vendorIcon);
297
297
  var title = document.createElement("span"); title.className = "split-pane-title"; title.textContent = pane.title; title.title = "Rename session";
298
298
  title.addEventListener("click", function () { if (title.getAttribute('aria-disabled') === 'true' || !syncPaneHeaderState(paneEl)) return; var current = sessionById(Number(paneEl.dataset.sessionId)); if (current) startPaneRename(header, title, { sessionId: current.id, title: current.title }); }); header.appendChild(title);
299
+ var dragSpace = document.createElement("span"); dragSpace.className = "split-pane-drag-space"; dragSpace.title = "Drag to rearrange panes"; dragSpace.setAttribute("aria-hidden", "true"); header.appendChild(dragSpace);
299
300
  var frame = document.createElement("iframe"); frame.className = "split-pane-frame"; frame.dataset.projectSlug = pane.slug;
300
301
  frame.dataset.sessionId = String(pane.sessionId); frame.src = paneUrl(pane); frame.title = pane.title;
301
302
  frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
@@ -0,0 +1,47 @@
1
+ import { store } from './store.js';
2
+ import { currentLayoutScope } from './split-view-layout-scope.js';
3
+
4
+ export function clearEmptySplit(host) {
5
+ var pane = host && host.querySelector(':scope > .split-session-empty-pane');
6
+ var menu = document.getElementById('split-view-create-menu');
7
+ if (pane && menu && pane.contains(menu)) {
8
+ menu.hidden = true;
9
+ if (menu.contains(document.activeElement)) document.activeElement.blur();
10
+ if (pane.contains(menu)) document.body.appendChild(menu);
11
+ }
12
+ if (pane) pane.remove();
13
+ var divider = host && host.querySelector(':scope > .split-session-reserved-divider');
14
+ if (divider) divider.remove();
15
+ }
16
+
17
+ export function reserveEmptySplit(host, geometry, inset) {
18
+ var state = store.get('splitSessionPicker');
19
+ var receipt = state && state.receipt;
20
+ var target = receipt && geometry.panes[receipt.origin];
21
+ if (!state || !state.side || !target || !currentLayoutScope(receipt.scope)) { clearEmptySplit(host); return false; }
22
+ var side = state.side, vertical = side === 'top' || side === 'bottom', before = side === 'left' || side === 'top';
23
+ var size = vertical ? 'height' : 'width', axis = vertical ? 'y' : 'x';
24
+ var half = Math.max(0, (target[size] - 7) / 2);
25
+ var original = Object.assign({}, target), empty = Object.assign({}, target), gap = Object.assign({}, target);
26
+ original[size] = half; empty[size] = half; gap[size] = 7;
27
+ gap[axis] += half;
28
+ if (before) original[axis] += half + 7; else empty[axis] += half + 7;
29
+ geometry.panes[receipt.origin] = original;
30
+ var pane = host.querySelector(':scope > .split-session-empty-pane');
31
+ if (!pane) {
32
+ pane = document.createElement('section'); pane.className = 'split-session-empty-pane';
33
+ pane.innerHTML = '<div class="split-session-empty-body"></div>';
34
+ host.appendChild(pane);
35
+ }
36
+ pane.dataset.side = side; pane.setAttribute('aria-label', 'Empty ' + side + ' split');
37
+ var menu = document.getElementById('split-view-create-menu');
38
+ var body = pane.querySelector('.split-session-empty-body');
39
+ if (menu && menu.parentNode !== body) body.appendChild(menu);
40
+ var divider = host.querySelector(':scope > .split-session-reserved-divider');
41
+ if (!divider) { divider = document.createElement('div'); divider.className = 'split-session-reserved-divider'; divider.setAttribute('aria-hidden', 'true'); host.appendChild(divider); }
42
+ [[pane, empty], [divider, gap]].forEach(function (entry) {
43
+ entry[0].style.left = inset.left + entry[1].x + 'px'; entry[0].style.top = inset.top + entry[1].y + 'px';
44
+ entry[0].style.width = entry[1].width + 'px'; entry[0].style.height = entry[1].height + 'px';
45
+ });
46
+ return true;
47
+ }